Repository: senghoo/wordai Branch: master Commit: 2210cc558409 Files: 61 Total size: 92.3 KB Directory structure: gitextract_fuu3vrhz/ ├── .dockerignore ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── app.py ├── commands/ │ └── __init__.py ├── config/ │ ├── __init__.py │ └── wsgi.ini ├── data/ │ ├── __init__.py │ └── dict.py ├── jsondict/ │ ├── anki.py │ └── search.py ├── manage.py ├── nginx-server.conf ├── nginx.conf ├── requirements.txt ├── start.sh ├── ui/ │ ├── .babelrc │ ├── .editorconfig │ ├── .eslintignore │ ├── .eslintrc.js │ ├── .gitignore │ ├── .postcssrc.js │ ├── README.md │ ├── build/ │ │ ├── build.js │ │ ├── check-versions.js │ │ ├── utils.js │ │ ├── vue-loader.conf.js │ │ ├── webpack.base.conf.js │ │ ├── webpack.dev.conf.js │ │ └── webpack.prod.conf.js │ ├── config/ │ │ ├── dev.env.js │ │ ├── index.js │ │ └── prod.env.js │ ├── index.html │ ├── package.json │ ├── src/ │ │ ├── App.vue │ │ ├── components/ │ │ │ ├── Card.vue │ │ │ ├── Dictionary.vue │ │ │ ├── Login.vue │ │ │ ├── Main.vue │ │ │ ├── statistic/ │ │ │ │ ├── DayChart.vue │ │ │ │ └── index.vue │ │ │ └── wordlist/ │ │ │ ├── Form.vue │ │ │ ├── Learned.vue │ │ │ ├── List.vue │ │ │ ├── Setting.vue │ │ │ ├── ToLearn.vue │ │ │ └── index.vue │ │ ├── main.js │ │ ├── permission.js │ │ ├── request.js │ │ ├── router/ │ │ │ └── index.js │ │ └── store.js │ └── static/ │ └── .gitkeep └── wordai/ ├── __init__.py ├── api/ │ ├── __init__.py │ └── apis.py └── models/ ├── __init__.py └── models.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ ui/node_modules ================================================ FILE: .gitignore ================================================ ### https://raw.github.com/github/gitignore/f9291de89f5f7dc0d3d87f9eb111b839f81d5dbc/Global/Emacs.gitignore # -*- mode: gitignore; -*- *~ \#*\# /.emacs.desktop /.emacs.desktop.lock *.elc auto-save-list tramp .\#* # Org-mode .org-id-locations *_archive # flymake-mode *_flymake.* # eshell files /eshell/history /eshell/lastdir # elpa packages /elpa/ # reftex files *.rel # AUCTeX auto folder /auto/ # cask packages .cask/ dist/ # Flycheck flycheck_*.el # server auth directory /server/ # projectiles files .projectile # directory configuration .dir-locals.el # network security /network-security.data ### https://raw.github.com/github/gitignore/f9291de89f5f7dc0d3d87f9eb111b839f81d5dbc/Python.gitignore # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ /data/data/ .DS_Store */.DS_Store ================================================ FILE: Dockerfile ================================================ FROM python:3.7 RUN curl -sL https://deb.nodesource.com/setup_12.x | bash - RUN apt-get update && apt-get install -y\ build-essential libpq-dev nodejs nginx-extras ENV NODE_PATH /usr/lib/node_modules RUN pip install uwsgi WORKDIR /usr/src/app COPY requirements.txt ./ RUN pip install --no-cache-dir -r requirements.txt COPY ui/package.json ./ui/ WORKDIR /usr/src/app/ui RUN npm install COPY . /usr/src/app ENV NODE_ENV production RUN npm run build WORKDIR /usr/src/app COPY nginx.conf /etc/nginx/ COPY nginx-server.conf /etc/nginx/sites-enabled/default EXPOSE 80 CMD './start.sh' ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2019 Senghoo Kim Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Word AI 一款使用自然语言处理辅助背单词的程序。 通过导入TMX翻译语料库,进行分词、词素分析、单词还原等操作自动生成各种不同语态、情态下的英语填空来进行单词学习。 *PS: 因为版权原因,不提供词典文件和TMX语料文件。需要自行下载导入。业余项目,分享出来供学习和交流,提供有限的技术支持,如有疑问欢迎Issue。* ## 功能特色 * 根据中文填写英文句子中的单词。 * 根据语料库自动生成题目,*拒绝重复* * 根据艾宾浩斯记忆曲线重复出现需要复习的单词。 * 对于错误单词自动弹出词典,进行学习。 * 支持自定义词库。 * B/S架构,兼容移动端和PC端。 ## 个人对背单词的一点看法 1. 背单词不能只针对独立的单词,需要放到语境中学习。 2. 单词需要自己完整的拼写出来,不能是只进行选择题。不然实际应用中容易出现拼写错误的情况。 3. 单词记忆时要关注时态、语态。 4. 学习的内容不能重复,也就是每次学习的上下文要发生改变,不能是固定的句子,学习固定的单词。这种情况下容易脱离了当前上下文,还是想不起单词。 ## Examples ### 单词学习 如遇到不会的单词,可以长按空格查看答案。 ![练习](doc/word.gif?raw=true) 对于错误单词自动弹出词典以供学习。 ![错误单词](doc/error.gif?raw=true) ### 自定义单词列表 文本框输入单词列表。一行一个,自动提示词典查询结果,以检查是否添加正确。 ![单词列表](doc/newlist.gif?raw=true) ### 学习统计 提供简单的统计分析功能。 ![单词列表](doc/analysis.gif?raw=true) ## 管理命令 因为是简单自用为目的设计的软件,没有单独做管理页面。系统管理通过命令行进行。 添加用户 ``` # 普通用户 python manage.py useradd <用户名> <口令> # 管理员 python manage.py useradd <用户名> <口令> admin ``` 导入字典 ``` python manage.py sync_dict ``` 导入例句 ``` python manage.py sync_sentence ``` 分析词典 ``` python manage.py wordlist # 标注 单词星级 python manage.py dict_parse # 拆分中英文解释 ``` ================================================ FILE: app.py ================================================ from wordai.api import app from config import api_env, flask_config app.config.update(flask_config()) def run_dev(): app.run(**api_env()) ================================================ FILE: commands/__init__.py ================================================ import string from data import load_dict, load_sentence from data.dict import Description from wordai.models import Word, Sentence, User, WordList, DictDescInfo def dict_to_mongo(): items = load_dict() for word, item in items.items(): if Word.objects(word=word).count() == 0: dbitem = Word(**item) dbitem.save() def sentence_to_mongo(*typ): items = load_sentence(*typ) print(items) for k, v in items.items(): _sentence_to_mongo(k, v) def _sentence_to_mongo(typ, items): import nltk from nltk.corpus import wordnet def wordnet_pos(tag): if tag.startswith('J'): return wordnet.ADJ elif tag.startswith('V'): return wordnet.VERB elif tag.startswith('N'): return wordnet.NOUN elif tag.startswith('R'): return wordnet.ADV else: return wordnet.NOUN # nltk.download('punkt') nltk.download('averaged_perceptron_tagger') nltk.download('stopwords') nltk.download('wordnet') nltk.download('punkt') stop_words = set(nltk.corpus.stopwords.words('english')) stemmer = nltk.stem.WordNetLemmatizer() sentences = [] for trans in items: eng, chn = trans.getsource(), trans.gettarget() tokens = nltk.word_tokenize(eng) pos_tag = [pos[1] for pos in nltk.pos_tag(tokens)] roots = [stemmer.lemmatize(word, wordnet_pos(pos_tag[idx])) for idx, word in enumerate(tokens)] cleanword = [token for token in roots if token.isalpha() and token not in stop_words and len(token) >= 3] # remove duplicates clean_word = list(dict.fromkeys(cleanword)) if len(clean_word) > 0: score = Word.search_words(*clean_word).sum('star') / len(clean_word) else: score = -1 sentence = Sentence(eng=eng, chn=chn, words=tokens, pos_tag=pos_tag, roots=roots, score=score, typ=typ) sentences.append(sentence) if len(sentences) > 50: Sentence.objects.insert(sentences) sentences = [] def new_user(username, passwd, role='user', *args): u = User(username=username, password=passwd, role=role) u.save() def dict_star_word_list(): for star in range(1, 6, 1): words = [word.word for word in Word.objects(star=star).only('word')] wl = WordList(name="word star {}".format(star), words=words) wl.save() def dict_parse(): for word in Word.objects: for des in word.descriptions: res = Description(des.description) des.seq = res.seq des.cn = res.cn des.en = res.en des.speech = res.speech des.infos = [DictDescInfo(**info) for info in res.info] word.save() def run(method, *args): if method == "sync_dict": dict_to_mongo() elif method == "sync_sentence": sentence_to_mongo(*args) elif method == "useradd": new_user(*args) elif method == "wordlist": dict_star_word_list(*args) elif method == "dict_parse": dict_parse(*args) else: print("unknown command {0}".format(method)) ================================================ FILE: config/__init__.py ================================================ import os def c(name, default=None): return os.environ.get(name.upper(), default) def mongo_config(): return { 'host': c('mongo_host', "127.0.0.1"), 'port': int(c('mongo_port', "27017")), 'db': c('mongo_db', "wordai"), 'username': c('mongo_username', "root"), 'password': c('mongo_password', ""), } def api_env(): return { 'host': c('host', '127.0.0.1'), 'port': int(c('port', 8000)), 'debug': c('debug', "true") == "true" } def flask_config(): return { 'JWT_SECRET_KEY': c('secret', '__SOME_SECRET_KEYS_HERE__') } ================================================ FILE: config/wsgi.ini ================================================ [uwsgi] module = app:app master = true processes = 3 socket = /var/run/wordai.sock logto = /var/log/wordai.log chmod-socket = 660 vacuum = true ================================================ FILE: data/__init__.py ================================================ # -*- coding: utf-8 -*- import os import json from itertools import chain from translate.storage.tmx import tmxfile DATA_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data") def data_file(filename): return os.path.join(DATA_DIR, filename) def load_dict(): with open(data_file("dict.json"), "r") as f: return json.load(f) def load_sentence(*typ): files = { 'talks': ['日常口语_20190906111009_1.tmx', '日常口语_20190906111009_2.tmx', '日常口语_20190906111009_3.tmx'], 'dictexams': ['词典例句汇集1.tmx', '词典例句汇集3.tmx', '词典例句汇集5.tmx', '词典例句汇集7.tmx', '词典例句汇集2.tmx', '词典例句汇集4.tmx', '词典例句汇集6.tmx', '词典例句汇集8.tmx'] } iters = {} for t, fs in files.items(): if len(typ) == 0 or t in typ: type_iterns = [] for fname in fs: with open(data_file(fname), 'rb') as fin: tmx = tmxfile(fin, 'en', 'cn') type_iterns.append(tmx.unit_iter()) iters[t] = chain(*type_iterns) return iters ================================================ FILE: data/dict.py ================================================ # -*- coding: utf-8 -*- import string import re class Description(object): def __init__(self, des): self.des = des self.idx = 0 self.length = len(des) self.info = [] if not self.read_seq(): self.seq = 0 self.en = self.des[self.idx:] self.cn = self.des[self.idx:] self.speech = 'OTHER' return if not self.read_word_speech(): self.en = self.des[self.idx:] self.cn = self.des[self.idx:] self.speech = 'OTHER' return if not self.read_cn_and_en(): self.en = self.des[self.idx:] self.cn = self.des[self.idx:] return self.read_info() def read_seq(self): res = '' while(self.idx < self.length): if self.des[self.idx] <= '9' and self.des[self.idx] >= '0': res += self.des[self.idx] self.idx += 1 else: break if self.des[self.idx] == '.': self.idx += 1 if len(res) > 0: self.seq = int(res.strip()) return True return False def read_word_speech(self): res = '' while(self.idx < self.length): if self.des[self.idx] in string.ascii_uppercase+"- ;": res += self.des[self.idx] self.idx += 1 else: break res = res.strip() if self.des[self.idx] == '\t': self.idx += 1 if len(res) > 0: self.speech = res return True return False def read_cn_and_en(self): end = self.des.find('【', self.idx) if end == -1: end = len(self.des) idx = end while(idx >= self.idx): idx -= 1 if self.des[idx] not in string.printable: break if idx == self.idx: self.cn = '' self.en = self.des[idx:end] elif idx == end-1: self.cn = self.des[idx:end] self.en = self.des[idx:end] else: self.cn = self.des[self.idx:idx+1] self.en = self.des[idx+1:end] self.idx = end return True def read_info(self): text = self.des[self.idx:] res = re.findall(r'【([^【】]+)】:([^【】]+)', text, re.M) for item in res: self.info.append({ 'name': item[0], 'value': item[1], }) ================================================ FILE: jsondict/anki.py ================================================ import sys import csv from search import search_string, description res = [] def cloze_word(sentense, word, level="1"): return sentense.lower().replace(word, "{{c"+level+"::"+word+"}}") with open(sys.argv[1], 'r') as f: contents = f.read() items = contents.split("\n\n") for item in items: lines = item.split("\n") word = lines[0] exp = lines[1] eg = lines[2:] dct = search_string(word) exp2 = cloze_word(description(word), word, "1") for x in [{"sentence": cloze_word(x, word, "1"),"exp2": exp2, "exp": exp, "dict": dct } for x in eg]: res.append(x) with open('{0}.csv'.format(sys.argv[1]), 'w') as csv_file: fieldnames = ['sentence','exp2', 'exp', 'dict'] writer = csv.DictWriter(csv_file, fieldnames=fieldnames) for row in res: print(row['sentence']) writer.writerows(res) ================================================ FILE: jsondict/search.py ================================================ #!/usr/bin/python # -*- coding: utf-8 -*- import sys import json with open('dict.json', 'r') as f: dictionary = json.load(f) def search(word): try: return dictionary[word] except: return None def format(item): res = "{word}\tStar:{star}\n".format(**item) for desc in item['descriptions']: res += "\t{description}\n".format(**desc) for ex in desc['examples']: res += "\t\tEN:\t{en}\n\t\tCN:\t{cn}\n".format(**ex) res +="\n" return res def description(word): item = search(word) if item is None: return "" return "\n".join([x['description'] for x in item['descriptions']]) def search_string(word): res = search(word) if res is None: return "" return format(res) if __name__ == '__main__': res = search(sys.argv[1]) if res is not None: print(format(res)) ================================================ FILE: manage.py ================================================ import os import sys ROOT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, ROOT_DIR) import commands import app def command(method, *args): if method == 'task': commands.run(*args) elif method == 'dev_server': app.run_dev() else: print("unknown command {0}".format(method)) def usage(): print("{0} task".format( sys.argv[0])) if __name__ == '__main__': if len(sys.argv) > 1: command(*sys.argv[1:]) else: usage() ================================================ FILE: nginx-server.conf ================================================ server { listen 80; server_name 0.0.0.0; location /api/ { include uwsgi_params; uwsgi_pass unix:/var/run/wordai.sock; } location / { root /usr/src/app/ui/dist/; } } ================================================ FILE: nginx.conf ================================================ user root; worker_processes auto; pid /run/nginx.pid; include /etc/nginx/modules-enabled/*.conf; events { worker_connections 768; } http { sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 65; types_hash_max_size 2048; include /etc/nginx/mime.types; default_type application/octet-stream; access_log /var/log/nginx/access.log; error_log /var/log/nginx/error.log; gzip on; include /etc/nginx/conf.d/*.conf; include /etc/nginx/sites-enabled/*; } ================================================ FILE: requirements.txt ================================================ mongoengine mongoengine-goodjson lxml translate-toolkit nltk flask flask-restful flask-jwt-extended flask-cors bcrypt jsonschema ================================================ FILE: start.sh ================================================ #!/bin/bash service nginx start uwsgi --ini config/wsgi.ini ================================================ FILE: ui/.babelrc ================================================ { "presets": [ ["env", { "modules": false, "targets": { "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] } }], "stage-2" ], "plugins": ["transform-vue-jsx", "transform-runtime"] } ================================================ FILE: ui/.editorconfig ================================================ root = true [*] charset = utf-8 indent_style = space indent_size = 2 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true ================================================ FILE: ui/.eslintignore ================================================ /build/ /config/ /dist/ /*.js ================================================ FILE: ui/.eslintrc.js ================================================ // https://eslint.org/docs/user-guide/configuring module.exports = { root: true, parserOptions: { parser: 'babel-eslint' }, env: { browser: true, }, extends: [ // https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention // consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules. 'plugin:vue/essential', // https://github.com/standard/standard/blob/master/docs/RULES-en.md 'standard' ], // required to lint *.vue files plugins: [ 'vue' ], // add your custom rules here rules: { // allow async-await 'generator-star-spacing': 'off', // allow debugger during development 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' } } ================================================ FILE: ui/.gitignore ================================================ .DS_Store node_modules/ /dist/ npm-debug.log* yarn-debug.log* yarn-error.log* # Editor directories and files .idea .vscode *.suo *.ntvs* *.njsproj *.sln ================================================ FILE: ui/.postcssrc.js ================================================ // https://github.com/michael-ciniawsky/postcss-load-config module.exports = { "plugins": { "postcss-import": {}, "postcss-url": {}, // to edit target browsers: use "browserslist" field in package.json "autoprefixer": {} } } ================================================ FILE: ui/README.md ================================================ # wordai > Word AI ## Build Setup ``` bash # install dependencies npm install # serve with hot reload at localhost:8080 npm run dev # build for production with minification npm run build # build for production and view the bundle analyzer report npm run build --report ``` For a detailed explanation on how things work, check out the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader). ================================================ FILE: ui/build/build.js ================================================ 'use strict' require('./check-versions')() process.env.NODE_ENV = 'production' const ora = require('ora') const rm = require('rimraf') const path = require('path') const chalk = require('chalk') const webpack = require('webpack') const config = require('../config') const webpackConfig = require('./webpack.prod.conf') const spinner = ora('building for production...') spinner.start() rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { if (err) throw err webpack(webpackConfig, (err, stats) => { spinner.stop() if (err) throw err process.stdout.write(stats.toString({ colors: true, modules: false, children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build. chunks: false, chunkModules: false }) + '\n\n') if (stats.hasErrors()) { console.log(chalk.red(' Build failed with errors.\n')) process.exit(1) } console.log(chalk.cyan(' Build complete.\n')) console.log(chalk.yellow( ' Tip: built files are meant to be served over an HTTP server.\n' + ' Opening index.html over file:// won\'t work.\n' )) }) }) ================================================ FILE: ui/build/check-versions.js ================================================ 'use strict' const chalk = require('chalk') const semver = require('semver') const packageConfig = require('../package.json') const shell = require('shelljs') function exec (cmd) { return require('child_process').execSync(cmd).toString().trim() } const versionRequirements = [ { name: 'node', currentVersion: semver.clean(process.version), versionRequirement: packageConfig.engines.node } ] if (shell.which('npm')) { versionRequirements.push({ name: 'npm', currentVersion: exec('npm --version'), versionRequirement: packageConfig.engines.npm }) } module.exports = function () { const warnings = [] for (let i = 0; i < versionRequirements.length; i++) { const mod = versionRequirements[i] if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { warnings.push(mod.name + ': ' + chalk.red(mod.currentVersion) + ' should be ' + chalk.green(mod.versionRequirement) ) } } if (warnings.length) { console.log('') console.log(chalk.yellow('To use this template, you must update following to modules:')) console.log() for (let i = 0; i < warnings.length; i++) { const warning = warnings[i] console.log(' ' + warning) } console.log() process.exit(1) } } ================================================ FILE: ui/build/utils.js ================================================ 'use strict' const path = require('path') const config = require('../config') const ExtractTextPlugin = require('extract-text-webpack-plugin') const packageConfig = require('../package.json') exports.assetsPath = function (_path) { const assetsSubDirectory = process.env.NODE_ENV === 'production' ? config.build.assetsSubDirectory : config.dev.assetsSubDirectory return path.posix.join(assetsSubDirectory, _path) } exports.cssLoaders = function (options) { options = options || {} const cssLoader = { loader: 'css-loader', options: { sourceMap: options.sourceMap } } const postcssLoader = { loader: 'postcss-loader', options: { sourceMap: options.sourceMap } } // generate loader string to be used with extract text plugin function generateLoaders (loader, loaderOptions) { const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader] if (loader) { loaders.push({ loader: loader + '-loader', options: Object.assign({}, loaderOptions, { sourceMap: options.sourceMap }) }) } // Extract CSS when that option is specified // (which is the case during production build) if (options.extract) { return ExtractTextPlugin.extract({ use: loaders, fallback: 'vue-style-loader' }) } else { return ['vue-style-loader'].concat(loaders) } } // https://vue-loader.vuejs.org/en/configurations/extract-css.html return { css: generateLoaders(), postcss: generateLoaders(), less: generateLoaders('less'), sass: generateLoaders('sass', { indentedSyntax: true }), scss: generateLoaders('sass'), stylus: generateLoaders('stylus'), styl: generateLoaders('stylus') } } // Generate loaders for standalone style files (outside of .vue) exports.styleLoaders = function (options) { const output = [] const loaders = exports.cssLoaders(options) for (const extension in loaders) { const loader = loaders[extension] output.push({ test: new RegExp('\\.' + extension + '$'), use: loader }) } return output } exports.createNotifierCallback = () => { const notifier = require('node-notifier') return (severity, errors) => { if (severity !== 'error') return const error = errors[0] const filename = error.file && error.file.split('!').pop() notifier.notify({ title: packageConfig.name, message: severity + ': ' + error.name, subtitle: filename || '', icon: path.join(__dirname, 'logo.png') }) } } ================================================ FILE: ui/build/vue-loader.conf.js ================================================ 'use strict' const utils = require('./utils') const config = require('../config') const isProduction = process.env.NODE_ENV === 'production' const sourceMapEnabled = isProduction ? config.build.productionSourceMap : config.dev.cssSourceMap module.exports = { loaders: utils.cssLoaders({ sourceMap: sourceMapEnabled, extract: isProduction }), cssSourceMap: sourceMapEnabled, cacheBusting: config.dev.cacheBusting, transformToRequire: { video: ['src', 'poster'], source: 'src', img: 'src', image: 'xlink:href' } } ================================================ FILE: ui/build/webpack.base.conf.js ================================================ 'use strict' const path = require('path') const utils = require('./utils') const config = require('../config') const vueLoaderConfig = require('./vue-loader.conf') function resolve (dir) { return path.join(__dirname, '..', dir) } const createLintingRule = () => ({ test: /\.(js|vue)$/, loader: 'eslint-loader', enforce: 'pre', include: [resolve('src'), resolve('test')], options: { formatter: require('eslint-friendly-formatter'), emitWarning: !config.dev.showEslintErrorsInOverlay } }) module.exports = { context: path.resolve(__dirname, '../'), entry: { app: './src/main.js' }, output: { path: config.build.assetsRoot, filename: '[name].js', publicPath: process.env.NODE_ENV === 'production' ? config.build.assetsPublicPath : config.dev.assetsPublicPath }, resolve: { extensions: ['.js', '.vue', '.json'], alias: { 'vue$': 'vue/dist/vue.esm.js', '@': resolve('src'), } }, module: { rules: [ ...(config.dev.useEslint ? [createLintingRule()] : []), { test: /\.vue$/, loader: 'vue-loader', options: vueLoaderConfig }, { test: /\.s[a|c]ss$/, loader: 'style!css!sass' }, { test: /\.js$/, loader: 'babel-loader', include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')] }, { test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, loader: 'url-loader', options: { limit: 10000, name: utils.assetsPath('img/[name].[hash:7].[ext]') } }, { test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, loader: 'url-loader', options: { limit: 10000, name: utils.assetsPath('media/[name].[hash:7].[ext]') } }, { test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, loader: 'url-loader', options: { limit: 10000, name: utils.assetsPath('fonts/[name].[hash:7].[ext]') } } ] }, node: { // prevent webpack from injecting useless setImmediate polyfill because Vue // source contains it (although only uses it if it's native). setImmediate: false, // prevent webpack from injecting mocks to Node native modules // that does not make sense for the client dgram: 'empty', fs: 'empty', net: 'empty', tls: 'empty', child_process: 'empty' } } ================================================ FILE: ui/build/webpack.dev.conf.js ================================================ 'use strict' const utils = require('./utils') const webpack = require('webpack') const config = require('../config') const merge = require('webpack-merge') const path = require('path') const baseWebpackConfig = require('./webpack.base.conf') const CopyWebpackPlugin = require('copy-webpack-plugin') const HtmlWebpackPlugin = require('html-webpack-plugin') const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') const portfinder = require('portfinder') const HOST = process.env.HOST const PORT = process.env.PORT && Number(process.env.PORT) const devWebpackConfig = merge(baseWebpackConfig, { module: { rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true }) }, // cheap-module-eval-source-map is faster for development devtool: config.dev.devtool, // these devServer options should be customized in /config/index.js devServer: { clientLogLevel: 'warning', historyApiFallback: { rewrites: [ { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') }, ], }, hot: true, contentBase: false, // since we use CopyWebpackPlugin. compress: true, host: HOST || config.dev.host, port: PORT || config.dev.port, open: config.dev.autoOpenBrowser, overlay: config.dev.errorOverlay ? { warnings: false, errors: true } : false, publicPath: config.dev.assetsPublicPath, proxy: config.dev.proxyTable, quiet: true, // necessary for FriendlyErrorsPlugin watchOptions: { poll: config.dev.poll, } }, plugins: [ new webpack.DefinePlugin({ 'process.env': require('../config/dev.env') }), new webpack.HotModuleReplacementPlugin(), new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update. new webpack.NoEmitOnErrorsPlugin(), // https://github.com/ampedandwired/html-webpack-plugin new HtmlWebpackPlugin({ filename: 'index.html', template: 'index.html', inject: true }), // copy custom static assets new CopyWebpackPlugin([ { from: path.resolve(__dirname, '../static'), to: config.dev.assetsSubDirectory, ignore: ['.*'] } ]) ] }) module.exports = new Promise((resolve, reject) => { portfinder.basePort = process.env.PORT || config.dev.port portfinder.getPort((err, port) => { if (err) { reject(err) } else { // publish the new Port, necessary for e2e tests process.env.PORT = port // add port to devServer config devWebpackConfig.devServer.port = port // Add FriendlyErrorsPlugin devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({ compilationSuccessInfo: { messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`], }, onErrors: config.dev.notifyOnErrors ? utils.createNotifierCallback() : undefined })) resolve(devWebpackConfig) } }) }) ================================================ FILE: ui/build/webpack.prod.conf.js ================================================ 'use strict' const path = require('path') const utils = require('./utils') const webpack = require('webpack') const config = require('../config') const merge = require('webpack-merge') const baseWebpackConfig = require('./webpack.base.conf') const CopyWebpackPlugin = require('copy-webpack-plugin') const HtmlWebpackPlugin = require('html-webpack-plugin') const ExtractTextPlugin = require('extract-text-webpack-plugin') const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') const UglifyJsPlugin = require('uglifyjs-webpack-plugin') const env = require('../config/prod.env') const webpackConfig = merge(baseWebpackConfig, { module: { rules: utils.styleLoaders({ sourceMap: config.build.productionSourceMap, extract: true, usePostCSS: true }) }, devtool: config.build.productionSourceMap ? config.build.devtool : false, output: { path: config.build.assetsRoot, filename: utils.assetsPath('js/[name].[chunkhash].js'), chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') }, plugins: [ // http://vuejs.github.io/vue-loader/en/workflow/production.html new webpack.DefinePlugin({ 'process.env': env }), new UglifyJsPlugin({ uglifyOptions: { compress: { warnings: false } }, sourceMap: config.build.productionSourceMap, parallel: true }), // extract css into its own file new ExtractTextPlugin({ filename: utils.assetsPath('css/[name].[contenthash].css'), // Setting the following option to `false` will not extract CSS from codesplit chunks. // Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack. // It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`, // increasing file size: https://github.com/vuejs-templates/webpack/issues/1110 allChunks: true, }), // Compress extracted CSS. We are using this plugin so that possible // duplicated CSS from different components can be deduped. new OptimizeCSSPlugin({ cssProcessorOptions: config.build.productionSourceMap ? { safe: true, map: { inline: false } } : { safe: true } }), // generate dist index.html with correct asset hash for caching. // you can customize output by editing /index.html // see https://github.com/ampedandwired/html-webpack-plugin new HtmlWebpackPlugin({ filename: config.build.index, template: 'index.html', inject: true, minify: { removeComments: true, collapseWhitespace: true, removeAttributeQuotes: true // more options: // https://github.com/kangax/html-minifier#options-quick-reference }, // necessary to consistently work with multiple chunks via CommonsChunkPlugin chunksSortMode: 'dependency' }), // keep module.id stable when vendor modules does not change new webpack.HashedModuleIdsPlugin(), // enable scope hoisting new webpack.optimize.ModuleConcatenationPlugin(), // split vendor js into its own file new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', minChunks (module) { // any required modules inside node_modules are extracted to vendor return ( module.resource && /\.js$/.test(module.resource) && module.resource.indexOf( path.join(__dirname, '../node_modules') ) === 0 ) } }), // extract webpack runtime and module manifest to its own file in order to // prevent vendor hash from being updated whenever app bundle is updated new webpack.optimize.CommonsChunkPlugin({ name: 'manifest', minChunks: Infinity }), // This instance extracts shared chunks from code splitted chunks and bundles them // in a separate chunk, similar to the vendor chunk // see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk new webpack.optimize.CommonsChunkPlugin({ name: 'app', async: 'vendor-async', children: true, minChunks: 3 }), // copy custom static assets new CopyWebpackPlugin([ { from: path.resolve(__dirname, '../static'), to: config.build.assetsSubDirectory, ignore: ['.*'] } ]) ] }) if (config.build.productionGzip) { const CompressionWebpackPlugin = require('compression-webpack-plugin') webpackConfig.plugins.push( new CompressionWebpackPlugin({ asset: '[path].gz[query]', algorithm: 'gzip', test: new RegExp( '\\.(' + config.build.productionGzipExtensions.join('|') + ')$' ), threshold: 10240, minRatio: 0.8 }) ) } if (config.build.bundleAnalyzerReport) { const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin webpackConfig.plugins.push(new BundleAnalyzerPlugin()) } module.exports = webpackConfig ================================================ FILE: ui/config/dev.env.js ================================================ 'use strict' const merge = require('webpack-merge') const prodEnv = require('./prod.env') module.exports = merge(prodEnv, { NODE_ENV: '"development"', BASE_API: '"http://localhost:8000/api"', }) ================================================ FILE: ui/config/index.js ================================================ 'use strict' // Template version: 1.3.1 // see http://vuejs-templates.github.io/webpack for documentation. const path = require('path') module.exports = { dev: { // Paths assetsSubDirectory: 'static', assetsPublicPath: '/', proxyTable: {}, // Various Dev Server settings host: 'localhost', // can be overwritten by process.env.HOST port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined autoOpenBrowser: false, errorOverlay: true, notifyOnErrors: true, poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions- // Use Eslint Loader? // If true, your code will be linted during bundling and // linting errors and warnings will be shown in the console. useEslint: true, // If true, eslint errors and warnings will also be shown in the error overlay // in the browser. showEslintErrorsInOverlay: false, /** * Source Maps */ // https://webpack.js.org/configuration/devtool/#development devtool: 'cheap-module-eval-source-map', // If you have problems debugging vue-files in devtools, // set this to false - it *may* help // https://vue-loader.vuejs.org/en/options.html#cachebusting cacheBusting: true, cssSourceMap: true }, build: { // Template for index.html index: path.resolve(__dirname, '../dist/index.html'), // Paths assetsRoot: path.resolve(__dirname, '../dist'), assetsSubDirectory: 'static', assetsPublicPath: '/', /** * Source Maps */ productionSourceMap: true, // https://webpack.js.org/configuration/devtool/#production devtool: '#source-map', // Gzip off by default as many popular static hosts such as // Surge or Netlify already gzip all static assets for you. // Before setting to `true`, make sure to: // npm install --save-dev compression-webpack-plugin productionGzip: false, productionGzipExtensions: ['js', 'css'], // Run the build command with an extra argument to // View the bundle analyzer report after build finishes: // `npm run build --report` // Set to `true` or `false` to always turn it on or off bundleAnalyzerReport: process.env.npm_config_report } } ================================================ FILE: ui/config/prod.env.js ================================================ 'use strict' module.exports = { NODE_ENV: '"production"', BASE_API: '"/api"', } ================================================ FILE: ui/index.html ================================================ Word-AI
================================================ FILE: ui/package.json ================================================ { "name": "wordai", "version": "1.0.0", "description": "Word AI", "author": "Senghoo Kim ", "private": true, "scripts": { "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", "start": "npm run dev", "lint": "eslint --ext .js,.vue src", "build": "node build/build.js" }, "dependencies": { "axios": "^0.19.0", "element-ui": "^2.12.0", "font-awesome": "^4.7.0", "js-cookie": "^2.2.1", "moment-timezone": "^0.5.26", "vue": "^2.5.2", "vue-axios": "^2.1.4", "vue-cli": "^2.9.6", "vue-moment": "^4.0.0", "vue-router": "^3.0.1", "vue-underscore": "^0.1.4", "vuex": "^3.1.1" }, "devDependencies": { "autoprefixer": "^7.1.2", "babel-core": "^6.22.1", "babel-eslint": "^8.2.1", "babel-helper-vue-jsx-merge-props": "^2.0.3", "babel-loader": "^7.1.1", "babel-plugin-syntax-jsx": "^6.18.0", "babel-plugin-transform-runtime": "^6.22.0", "babel-plugin-transform-vue-jsx": "^3.5.0", "babel-preset-env": "^1.3.2", "babel-preset-stage-2": "^6.22.0", "chalk": "^2.0.1", "copy-webpack-plugin": "^4.0.1", "css-loader": "^0.28.11", "echarts": "^4.2.1", "eslint": "^4.15.0", "eslint-config-standard": "^10.2.1", "eslint-friendly-formatter": "^3.0.0", "eslint-loader": "^1.7.1", "eslint-plugin-import": "^2.7.0", "eslint-plugin-node": "^5.2.0", "eslint-plugin-promise": "^3.4.0", "eslint-plugin-standard": "^3.0.1", "eslint-plugin-vue": "^4.0.0", "extract-text-webpack-plugin": "^3.0.0", "file-loader": "^1.1.4", "friendly-errors-webpack-plugin": "^1.6.1", "html-webpack-plugin": "^2.30.1", "node-notifier": "^5.1.2", "node-sass": "^4.12.0", "optimize-css-assets-webpack-plugin": "^3.2.0", "ora": "^1.2.0", "portfinder": "^1.0.13", "postcss-import": "^11.0.0", "postcss-loader": "^2.0.8", "postcss-url": "^7.2.1", "rimraf": "^2.6.0", "sass-loader": "^7.3.1", "semver": "^5.3.0", "shelljs": "^0.7.8", "style-loader": "^1.0.0", "uglifyjs-webpack-plugin": "^1.1.1", "url-loader": "^0.5.8", "vue-loader": "^13.3.0", "vue-style-loader": "^3.0.1", "vue-template-compiler": "^2.5.2", "webpack": "^3.6.0", "webpack-bundle-analyzer": "^3.3.2", "webpack-dev-server": "^3.1.11", "webpack-merge": "^4.1.0" }, "engines": { "node": ">= 6.0.0", "npm": ">= 3.0.0" }, "browserslist": [ "> 1%", "last 2 versions", "not ie <= 8" ] } ================================================ FILE: ui/src/App.vue ================================================ ================================================ FILE: ui/src/components/Card.vue ================================================ ================================================ FILE: ui/src/components/Dictionary.vue ================================================ ================================================ FILE: ui/src/components/Login.vue ================================================ ================================================ FILE: ui/src/components/Main.vue ================================================ ================================================ FILE: ui/src/components/statistic/DayChart.vue ================================================ ================================================ FILE: ui/src/components/statistic/index.vue ================================================ ================================================ FILE: ui/src/components/wordlist/Form.vue ================================================ ================================================ FILE: ui/src/components/wordlist/Learned.vue ================================================ ================================================ FILE: ui/src/components/wordlist/List.vue ================================================ ================================================ FILE: ui/src/components/wordlist/Setting.vue ================================================ ================================================ FILE: ui/src/components/wordlist/ToLearn.vue ================================================ ================================================ FILE: ui/src/components/wordlist/index.vue ================================================ ================================================ FILE: ui/src/main.js ================================================ // The Vue build version to load with the `import` command // (runtime-only or standalone) has been set in webpack.base.conf with an alias. import Vue from 'vue' import VueAxios from 'vue-axios' import axios from 'axios' import underscore from 'vue-underscore' import ElementUI from 'element-ui' import 'element-ui/lib/theme-chalk/index.css' import App from './App' import router from './router' import store from './store' import request from './request' import '@/permission' Vue.use(ElementUI) Vue.use(VueAxios, request) Vue.use(underscore) import moment from 'moment-timezone' moment.tz.setDefault("UTC") require('moment/locale/zh-cn') Vue.use(require('vue-moment'), { moment }) Vue.config.productionTip = false /* eslint-disable no-new */ new Vue({ el: '#app', router, store, components: { App }, template: '' }) ================================================ FILE: ui/src/permission.js ================================================ import router from './router' import store from './store' import {getToken} from './store' import { Message } from 'element-ui' const whiteList = ['/login'] router.beforeEach((to, from, next) => { if (getToken()) { if (to.path === '/login') { next({ path: '/' }) } else { store.dispatch('getUserInfo').then(res => { next() }).catch((err) => { store.dispatch('logOut').then(() => { Message.error(err || 'Verification failed, please login again') next({ path: '/' }) }) }) } } else { if (whiteList.indexOf(to.path) !== -1) { next() } else { next('/login') } } }) ================================================ FILE: ui/src/request.js ================================================ import axios from 'axios' import {MessageBox} from 'element-ui' import store from './store' import { getToken } from './store' const service = axios.create({ baseURL: process.env.BASE_API }) var lastRefresh = (new Date()).getTime() service.interceptors.request.use(config => { if (getToken()) { if (!config.headers['Authorization']){ config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改 } var now = (new Date()).getTime() // debugger; if (now - lastRefresh > 1 * 60 * 1000 && config.url !== '/token/refresh' && config.url !== '/login') { store.dispatch('refreshJWT').then(() => { lastRefresh = now }) } } return config }, error => { // Do something with request error console.log(error) // for debug Promise.reject(error) }) service.interceptors.response.use( response => { return response }, error => { const req = error.request const res = error.response const url = new URL(req.responseURL) if (res.status === 401 && url.pathname !== '/login') { MessageBox.confirm('你已被登出', '确定登出', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' }).then(() => { store.dispatch('logOut').then(() => { location.reload()// 为了重新实例化vue-router对象 避免bug }) }) } return Promise.reject(error) } ) export default service ================================================ FILE: ui/src/router/index.js ================================================ import Vue from 'vue' import Router from 'vue-router' Vue.use(Router) export default new Router({ routes: [ { path: '/', name: 'HelloWorld', component: () => import('@/components/Main') }, { path: '/login', name: 'Login', component: () => import('@/components/Login') } ] }) ================================================ FILE: ui/src/store.js ================================================ import Vue from 'vue' import vuex from 'vuex' import service from './request' //import Cookies from 'js-cookie' // Vue.use(service) Vue.use(vuex) const TokenKey = 'accessToken' const refreshKey = 'refreshToken' export function getToken () { return sessionStorage.getItem(TokenKey) } export function getRefreshToken () { return sessionStorage.getItem(refreshKey) } export function setToken (token) { return sessionStorage.setItem(TokenKey, token) } export function setRefreshTokens (token) { return sessionStorage.setItem(refreshKey, token) } export function removeToken () { sessionStorage.removeItem(refreshKey) return sessionStorage.removeItem(TokenKey) } export default new vuex.Store({ state: { accessToken: '', refreshToken: '', wordlist: '' }, getters: { accessToken: state => state.accessToken, refreshToken: state => state.refreshToken, username: (state, getters) => state.accessToken ? JSON.parse(atob(getters.accessToken.split('.')[1])) : null, }, mutations: { setTokens (state, access) { // When this updates, the getters and anything bound to them updates as well. state.accessToken = access }, setRefreshTokens (state, refresh) { // When this updates, the getters and anything bound to them updates as well. state.refreshToken = refresh }, setWordList (state, wordlist) { state.wordList = wordlist } }, actions: { fetchJWT ({ commit }, { username, password }) { return service({ 'method': 'POST', 'url': '/login', 'headers': { 'Content-Type': 'application/json; charset=utf-8' }, 'data': { 'username': username, 'password': password } }).then(res => { commit('setTokens', res.data.access_token) commit('setRefreshTokens', res.data.refresh_token) setToken(res.data.access_token) setRefreshTokens(res.data.refresh_token) } ) }, refreshJWT ({ commit, state}) { return service({ 'method': 'POST', 'url': '/token/refresh', 'headers': { 'Content-Type': 'application/json; charset=utf-8', 'Authorization': 'Bearer ' + getRefreshToken() } }).then(res => { commit('setTokens', res.data.access_token) // commit('setRefreshTokens', res.data.refresh_token) setToken(res.data.access_token, res.data.refresh_token) }) }, getUserInfo ({commit, state}) { return service({ 'method': 'GET', 'url': '/user/wordlist', 'headers': { 'Content-Type': 'application/json; charset=utf-8', 'Authorization': 'Bearer ' + getToken() } }).then(res => { commit('setWordList', res.wordlist) }) }, logOut ({commit, state}) { return new Promise((resolve, reject) => { commit('setTokens', '', '') removeToken() resolve() }) } } }) ================================================ FILE: ui/static/.gitkeep ================================================ ================================================ FILE: wordai/__init__.py ================================================ ================================================ FILE: wordai/api/__init__.py ================================================ from flask import Flask, jsonify from flask_cors import CORS from flask_jwt_extended import (JWTManager) app = Flask(__name__) CORS(app) jwt = JWTManager(app) from wordai.models import User @app.route('/') def index(): return jsonify({'message': 'Hello, World!'}) from wordai.api.apis import blueprint as api app.register_blueprint(api, url_prefix='/api') ================================================ FILE: wordai/api/apis.py ================================================ import hashlib import json from datetime import datetime, timedelta import wordai.models as models from flask import Blueprint from flask_jwt_extended import (JWTManager, create_access_token, create_refresh_token, get_jwt_identity, get_raw_jwt, jwt_refresh_token_required, jwt_required) from flask_restful import Api, Resource, abort, reqparse, request from jsonschema import validate blueprint = Blueprint('profile', __name__, template_folder='templates', static_folder='static') api = Api(blueprint) class api_register(object): def __init__(self, path): self.path = path def __call__(self, cls): api.add_resource(cls, self.path) return cls def admin_required(f): def __inner__(self, *args, **kwargs): identify = get_jwt_identity() user = models.User.find_by_username(identify) if user and user.role == 'admin': return f(self, user, *args, **kwargs) return { 'message': 'Not found', }, 404 return jwt_required(__inner__) def user_required(f): def __inner__(self, *args, **kwargs): identify = get_jwt_identity() user = models.User.find_by_username(identify) if user and user.role in ['admin', 'user'] : return f(self, user, *args, **kwargs) return { 'message': 'Not found', }, 404 return jwt_required(__inner__) user_parser = reqparse.RequestParser() user_parser.add_argument('username', help='This username cannot be blank', required=True) user_parser.add_argument('password', help='This password cannot be blank', required=True) @api_register("/registration") class UserRegistration(Resource): def post(self): return {'message': 'User registration'} @api_register("/login") class UserLogin(Resource): def post(self): data = user_parser.parse_args() current_user = models.User.check_user(data['username'], data['password']) if not current_user: abort(401) return { 'message': 'User {} doesn\'t exist'.format(data['username']), } access_token = create_access_token(identity=data['username']) refresh_token = create_refresh_token(identity=data['username']) return { 'message': 'Logged in as {}'.format(current_user.username), 'role': current_user.role, 'access_token': access_token, 'refresh_token': refresh_token } @api_register("/token/refresh") class TokenRefresh(Resource): @jwt_refresh_token_required def post(self): current_user = get_jwt_identity() if current_user: access_token = create_access_token(identity=current_user) return { 'access_token': access_token} abort(401) return {'message': 'invalid refresh token'} @api_register("/wordlist") class WordListList(Resource): @user_required def get(self, user): return [json.loads(x.to_json()) for x in user.wordlists()] @user_required def put(self, user): schema = { "type": "array", "items": {"type": "string"}, "uniqueItems": True } try: body = request.json validate(instance=body, schema=schema) wordok, not_has, wnot_has = models.WordList.check_word(*body) defines = models.Word.search_words(*wordok) return { "defines": {w['word']: w for w in json.loads(defines.to_json())}, "not_dict": wnot_has, "not_sentence": not_has, } except Exception as err: return { "message": "invalid request body", "error": str(err) }, 422 @user_required def post(self, user): schema = { "type": "object", "properties": { "name": {"type": "string"}, "description": {"type": "string"}, "words": { "type": "array", "items": {"type": "string"}, "uniqueItems": True } } } try: body = request.json validate(instance=body, schema=schema) wordok, not_has, wnot_has = models.WordList.check_word(*body['words']) body['words'] = list(wordok) wordlist = models.WordList(**body) wordlist.user = user wordlist.save() return { "message": "ok", "has": list(wordok), "not_dict": wnot_has, "not_sentence": not_has, } except Exception as err: return { "message": "invalid request body", "error": str(err) }, 422 @api_register("/wordlist/") class WordListItem(Resource): @user_required def get(self, user, lid): print(lid) return json.loads(user.wordlists().filter(id=lid).first().to_json()) @user_required def put(self, user, lid): wordlist = models.WordList.objects(user=user, id=lid).first() if not wordlist: return { "message": "wordlist not exists", }, 404 schema = { "type": "object", "properties": { "name": {"type": "string"}, "description": {"type": "string"}, "words": { "type": "array", "items": {"type": "string"}, "uniqueItems": True } } } try: body = request.json validate(instance=body, schema=schema) wordok, not_has, wnot_has = models.WordList.check_word(*body['words']) wordlist.words = wordok wordlist.name = body['name'] wordlist.description = body['description'] wordlist.user = user wordlist.save() return { "message": "ok", "has": list(wordok), "not_dict": wnot_has, "not_sentence": not_has, } except Exception as err: return { "message": "invalid request body", "error": str(err) }, 422 @user_required def delete(self, user, lid): wordlist = models.WordList.objects(user=user, id=lid).first() if not wordlist: return { "message": "wordlist not exists", }, 404 wordlist.delete() @api_register("/user/wordlist") class UserWordList(Resource): @user_required def get(self, user): if not user.wordlist: return { "message", "wordlist not set" }, 404 data = json.loads(user.wordlist.to_json()) return { "message": "ok", "wordlist": data['id'], "wordlist_name": data['name'] } @user_required def post(self, user): parser = reqparse.RequestParser() parser.add_argument('wordlist', help='This wordlist cannot be blank', required=True) wordlist_id = parser.parse_args() wordlist = models.WordList.objects(id=wordlist_id['wordlist']).first() user.wordlist = wordlist user.save() return { "message": "ok", "wordlist": wordlist.name } @api_register("/learn/word") class LearnNext(Resource): @user_required def get(self, user): ex = user.next_exercise() if ex: sentence_id = json.loads(ex.sentence.to_json())['id'] word_id = json.loads(ex.word.to_json())['id'] return { "id": word_id, "word": ex.word.word, "message": "ok", "cloze": ex.cloze, "cn": ex.sentence.chn, "sid": sentence_id, "answers": [a for a in ex.answers], "check": [hashlib.sha1((a+sentence_id+word_id).encode()).hexdigest() for a in ex.answers] } else: return { "message": "no word need exercise" }, 404 @user_required def post(self, user): parser = reqparse.RequestParser() parser.add_argument('id', help='This answers cannot be blank', required=True) parser.add_argument('sid', help='This answers cannot be blank', required=True) parser.add_argument('answers', help='This answers cannot be blank', required=True,action='append') parser.add_argument('check', help='This answer_check cannot be blank', required=True, action='append') data = parser.parse_args() word_id = data['id'] word = models.Word.objects(id=word_id).first() if not word: return { "message": "word not exist" }, 404 sentence_id = data['sid'] answers = data['answers'] check = data['check'] check_res = [hashlib.sha1((a+sentence_id+word_id).encode()).hexdigest() for a in answers] result = check == check_res slog = models.SentenceLog(sentence=sentence_id, result=result, time=datetime.utcnow()) models.ExerciseLog.objects(user=user, word=word).update_one( push__sentences=slog, wordname=word.word, upsert=True) log = models.ExerciseLog.objects(user=user, word=word).first() log.calucate_review() log.save() return { "message": "ok", "result": result, } @api_register("/dictionary/") class Dictionary(Resource): @user_required def get(self, user, word): define = models.Word.objects(word=word).first() if define: return json.loads(define.to_json()) else: return {"message": "not found"}, 404 @api_register("/wordlist/learned") class WordlistLearned(Resource): @user_required def get(self, user): words = user.wordlist.user_learned(user).only("wordname", "review") return json.loads(words.to_json()) @api_register("/wordlist/to_learn") class WordlistToLearn(Resource): @user_required def get(self, user): words = user.wordlist.user_to_learn(user) return words @api_register("/statistic/learn") class StatisticLearn(Resource): @user_required def get(self, user): return { 'exercise': models.ExerciseLog.exercise_count( user, datetime.now()-timedelta(days=7), datetime.now()+timedelta(days=7) ), 'review': models.ExerciseLog.review_count( user, datetime.now()-timedelta(days=7), datetime.now()+timedelta(days=7) ) } ================================================ FILE: wordai/models/__init__.py ================================================ from mongoengine import connect from config import mongo_config connect(**mongo_config()) from .models import * ================================================ FILE: wordai/models/models.py ================================================ from datetime import datetime, timedelta import bcrypt from mongoengine import (BooleanField, DateTimeField, EmbeddedDocumentField, FloatField, IntField, ListField, ReferenceField, SortedListField, StringField) from mongoengine_goodjson import Document, EmbeddedDocument class DictExample(EmbeddedDocument): en = StringField(required=True) cn = StringField(required=True) class DictDescInfo(EmbeddedDocument): name = StringField(required=True) value = StringField(required=True) class DictDescription(EmbeddedDocument): description = StringField(required=True) seq = IntField(required=True) cn = StringField(required=True) en = StringField(required=True) speech = StringField(required=True) infos = ListField(EmbeddedDocumentField(DictDescInfo)) examples = ListField(EmbeddedDocumentField(DictExample)) class Word(Document): word = StringField(required=True) star = IntField(max_value=5) descriptions = ListField(EmbeddedDocumentField(DictDescription)) meta = { 'indexes': [ ('word', '-star'), ] } @classmethod def search_words(cls, *words): return Word.objects(word__in=words) @classmethod def has(cls, *words): has = [] not_has = [] for word in words: if Word.objects(word=word).count() > 0: has.append(word) else: not_has.append(word) return has, not_has @classmethod def search_word(cls, word): return Word.objects(word=word).first() class Sentence(Document): eng = StringField(required=True) chn = StringField(required=True) score = FloatField() words = ListField(StringField()) pos_tag = ListField(StringField()) roots = ListField(StringField()) typ = StringField(required=True) meta = { 'indexes': [ ('roots', '-score'), ('typ', 'roots', '-score') ] } @classmethod def has(cls, *words): has = [] not_has = [] for word in words: if cls.objects(roots=word).count() > 0: has.append(word) else: not_has.append(word) return has, not_has @classmethod def search_by_root(cls, word): return cls.objects(roots=word, typ='dictexams').order_by('-score') def cloze(self, word): answers = [] cloz = self.eng for idx, root in enumerate(self.roots): if root == word: answers.append(self.words[idx]) cloz = cloz.replace(self.words[idx], '[___]') return cloz, answers class User(Document): username = StringField(required=True, unique=True) encrypted_password = StringField(required=True) salt = StringField(required=True, default=lambda: bcrypt.gensalt().decode()) role = StringField(required=True, default='user') wordlist = ReferenceField('WordList', required=True) meta = { 'indexes': [ '#username' ] } def __init__(self, *args, **kwargs): passwd = kwargs.get("password") if passwd: del kwargs['password'] Document.__init__(self, *args, **kwargs) if passwd: self.password = passwd @classmethod def find_by_username(cls, username): return cls.objects(username=username).first() @classmethod def check_user(cls, username, passwd): user = cls.objects(username=username).first() if user and user.check_password(passwd): return user return None @property def password(self): return "" @password.setter def password(self, passwd): passwd = passwd.encode() salt = bcrypt.gensalt() self.encrypted_password = bcrypt.hashpw(passwd, salt).decode() def check_password(self, passwd): passwd = passwd.encode() if (not self.encrypted_password) or (not self.salt): return False # hashed = bcrypt.hashpw(passwd, self.salt) return bcrypt.checkpw(passwd, self.encrypted_password.encode()) def wordlist_exercise_log(self): list_words = self.wordlist.words return ExerciseLog.objects(user=self, wordname__in=list_words) def word_exercise_log(self, word): return ExerciseLog.objects(user=self, wordname=word).first() def new_words(self): list_words = self.wordlist.words learned = [l.wordname for l in self.wordlist_exercise_log().only('wordname')] return list(set(list_words) - set(learned)) def due_words(self): return [l.wordname for l in self.wordlist_exercise_log().filter(review__lt=datetime.utcnow()).only('wordname')] def next_word(self): words = self.due_words() if words : return words[0] words = self.new_words() if words : return words[0] return None def next_exercise(self): word = self.next_word() if not word: return None word_item = Word.search_word(word) if not word_item: return None now = datetime.utcnow() exlog = self.word_exercise_log(word) sentence_log = exlog.sentences if exlog else [] log = {l.sentence.id: score_w(now - l.time, l.result) for l in sentence_log} sentences = Sentence.search_by_root(word).limit(100) top = None top_score = 0 for s in sentences: score = s.score * log.get(s.id, 1) if score > top_score: top = s top_score = score if not top: return None class cloze: def __init__(self, word, quiz, answers, sentence): self.word = word self.cloze = quiz self.answers = answers self.sentence = sentence quiz, answer = top.cloze(word) return cloze(word_item, quiz, answer, top) def wordlists(self): return WordList.objects(user__in=[None, self]) class WordList(Document): name = StringField(required=True) description = StringField(required=True) words = ListField(StringField()) user = ReferenceField(User) meta = { 'indexes': [ ('user', 'name'), ] } @classmethod def check_word(self, *words): has, not_has = Sentence.has(*words) whas, wnot_has = Word.has(*words) has = set(has) whas = set(whas) ok = list(has.intersection(whas)) return ok, not_has, wnot_has def user_learned(self, user): return ExerciseLog.objects(user=user, wordname__in=list(self.words)) def user_to_learn(self, user): learned = set([e.wordname for e in ExerciseLog.objects(user=user).only('wordname')]) words = set(self.words) return list(words - learned) def score_w(delta, result): if delta < timedelta(minutes=20): w = (1-.58) elif delta < timedelta(hours=1): w = (1-.44) elif delta < timedelta(hours=9): w = (1-.36) elif delta < timedelta(days=1): w = (1-.33) elif delta < timedelta(days=2): w = (1-.28) elif delta < timedelta(days=6): w = (1-.25) elif delta < timedelta(days=31): w = (1-.21) elif delta < timedelta(days=60): w = (1-.10) else: w = .99 return w if result else w * 1.5 class SentenceLog(EmbeddedDocument): sentence = ReferenceField(Sentence, required=True) result = BooleanField(required=True) time = DateTimeField(required=True) ebbinghaus = { 0: timedelta(minutes=1), 1: timedelta(minutes=5), 2: timedelta(minutes=30), 3: timedelta(hours=12), 4: timedelta(days=1), 5: timedelta(days=2), 6: timedelta(days=4), 7: timedelta(days=7), 8: timedelta(days=15), } class ExerciseLog(Document): user = ReferenceField(User, required=True) wordname = StringField(required=True) word = ReferenceField(Word, required=True) review = DateTimeField(required=True) sentences = SortedListField( EmbeddedDocumentField(SentenceLog), ordering="time", reverse=True) def calucate_review(self): count = 0 for s in reversed(self.sentences): if not s.result: break count += 1 delta = ebbinghaus.get(count, timedelta(days=15)) self.review = datetime.utcnow() + delta @classmethod def review_count(cls, user, start, end): data = ExerciseLog.objects(user=user, review__gt=start, review__lt=end).\ aggregate( {'$group' : {'_id' : {'$dateToString' : { 'date': "$review", 'format': "%Y/%m/%d", } }, 'count' : { '$sum' : 1 }}}, {'$sort': {'_id': 1}} ) return {i['_id']:i['count'] for i in list(data)} @classmethod def exercise_count(cls, user, start, end): data = ExerciseLog.objects(user=user).\ aggregate( {"$match": {"sentences.time": {"$gte": start, "$lte": end}}}, {"$project":{'_id':0, 'sentences.time': 1, 'sentences.result':1}}, {"$unwind":"$sentences"}, {"$match": {"sentences.time": {"$gte": start, "$lte": end}}}, {'$project': {'day': '$sentences.time', 'result': '$sentences.result'}}, {'$group': { '_id': {'$dateToString' : { 'date': "$day", 'format': "%Y/%m/%d", } }, 'count': { '$sum': 1 } } }, {'$sort': {'_id': 1}} ) return {i['_id']:i['count'] for i in list(data)}