Created webapp for reviewing WaniKani burned items.
This commit is contained in:
commit
c6b94ee31c
110
.gitignore
vendored
Normal file
110
.gitignore
vendored
Normal file
@ -0,0 +1,110 @@
|
||||
# Created by .ignore support plugin (hsz.mobi)
|
||||
### JetBrains template
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio
|
||||
|
||||
*.iml
|
||||
|
||||
## Directory-based project format:
|
||||
.idea/
|
||||
# if you remove the above rule, at least ignore the following:
|
||||
|
||||
# User-specific stuff:
|
||||
# .idea/workspace.xml
|
||||
# .idea/tasks.xml
|
||||
# .idea/dictionaries
|
||||
|
||||
# Sensitive or high-churn files:
|
||||
# .idea/dataSources.ids
|
||||
# .idea/dataSources.xml
|
||||
# .idea/sqlDataSources.xml
|
||||
# .idea/dynamic.xml
|
||||
# .idea/uiDesigner.xml
|
||||
|
||||
# Gradle:
|
||||
# .idea/gradle.xml
|
||||
# .idea/libraries
|
||||
|
||||
# Mongo Explorer plugin:
|
||||
# .idea/mongoSettings.xml
|
||||
|
||||
## File-based project format:
|
||||
*.ipr
|
||||
*.iws
|
||||
|
||||
## Plugin-specific files:
|
||||
|
||||
# IntelliJ
|
||||
/out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
### Python template
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
env/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# 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/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*,cover
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
wanikani.db
|
||||
config.py
|
BIN
static/burned.png
Normal file
BIN
static/burned.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
39
static/style.css
Normal file
39
static/style.css
Normal file
@ -0,0 +1,39 @@
|
||||
.centered {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left:50%;
|
||||
transform: translate(-50%,-50%);
|
||||
}
|
||||
|
||||
.padded {
|
||||
padding: 5%;
|
||||
}
|
||||
|
||||
body{
|
||||
background-color: lightgray;
|
||||
}
|
||||
|
||||
.correct{
|
||||
background-color: greenyellow;
|
||||
}
|
||||
|
||||
.wrong{
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
.question-size{
|
||||
font-size: 1000%;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.answer-size{
|
||||
font-size: 200%;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.help-size{
|
||||
font-size: 200%;
|
||||
color: white;
|
||||
}
|
1
static/wanakana.min.js
vendored
Normal file
1
static/wanakana.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
127
templates/quiz.html
Normal file
127
templates/quiz.html
Normal file
@ -0,0 +1,127 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title></title>
|
||||
<!-- Latest compiled and minified CSS -->
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
|
||||
|
||||
<!-- Optional theme -->
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap-theme.min.css">
|
||||
<script src="https://code.jquery.com/jquery-2.1.4.min.js"></script>
|
||||
<!-- Latest compiled and minified JavaScript -->
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
|
||||
<script src="{{ url_for('static', filename='wanakana.min.js') }}"></script>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-default">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<a class="navbar-brand">
|
||||
<img alt="Brand" height="24" width="24" src="{{ url_for('static', filename='burned.png')}}">
|
||||
</a>
|
||||
</div>
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li>
|
||||
<button id="api-refresh" type="submit" class="btn btn-default navbar-btn" aria-label="Refresh API">
|
||||
<span class="glyphicon glyphicon-refresh" aria-hidden="true"></span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="navbar-text" data-toggle="tooltip" data-placement="bottom" title="Last API Refresh: {{ current_user.last_updated }}"><span><img alt="avatar" src="http://www.gravatar.com/avatar/{{ current_user.gravatar }}?s=24"></span> {{ current_user.username }}</li>
|
||||
<li><a href="/logout">Logout</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center"><p class="question-size" id="question-area"></p><img id="question-image" src=""></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="form-group">
|
||||
<input type="text" autocomplete=off id="kana" class="form-control answer-size">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center">
|
||||
<button class="btn btn-default btn-lg" id="submit-answer">Check</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div id="help-area" class="col-md-12 text-center help-size">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center">
|
||||
<button class="btn btn-default btn-lg" id="get-help">I Don't Know</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<script>
|
||||
var input = document.getElementById('kana');
|
||||
$(document).ready(function(){
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
refreshQuestion()
|
||||
}).keypress(function(e){
|
||||
if(e.which == 13){
|
||||
$("#submit-answer").click()
|
||||
}
|
||||
})
|
||||
function refreshQuestion(){
|
||||
$.getJSON("/quiz_item", function(data){
|
||||
$("#help-area").text('');
|
||||
$("#kana").removeClass("correct").val('');
|
||||
sessionStorage.setItem("question", data['question']);
|
||||
sessionStorage.setItem("answer", data['answer']);
|
||||
sessionStorage.setItem("item_type", data['item_type']);
|
||||
$("#question-area").text('');
|
||||
$("#question-image").attr("src", '');
|
||||
if(sessionStorage.getItem('item_type') == 'radical'){
|
||||
$("#kana").attr("placeholder", "Meaning")
|
||||
document.body.style.backgroundColor = "deepskyblue";
|
||||
wanakana.unbind(input);
|
||||
if(sessionStorage.getItem('question').indexOf('http') >= 0){
|
||||
$("#question-image").attr("src", sessionStorage.getItem('question'));
|
||||
}
|
||||
else {
|
||||
$("#question-area").text(sessionStorage.getItem('question'));
|
||||
}
|
||||
}
|
||||
else{
|
||||
$("#kana").attr("placeholder", "かな")
|
||||
if(sessionStorage.getItem('item_type') == 'kanji'){
|
||||
document.body.style.backgroundColor = "deeppink";
|
||||
}
|
||||
else{
|
||||
document.body.style.backgroundColor = "darkviolet";
|
||||
}
|
||||
wanakana.bind(input);
|
||||
$("#question-area").text(sessionStorage.getItem('question'));
|
||||
}
|
||||
})}
|
||||
$("#submit-answer").click(function (e){
|
||||
e.preventDefault();
|
||||
var input_answer = document.getElementById('kana').value;
|
||||
if($("#kana").hasClass("correct")){
|
||||
refreshQuestion();
|
||||
}
|
||||
else if(sessionStorage.getItem('answer').replace(/\s/g, '').split(/[,\.]/g).indexOf(input_answer) !== -1){
|
||||
$("#kana").addClass("correct").removeClass("wrong");
|
||||
}
|
||||
else{
|
||||
$("#kana").addClass("wrong").removeClass("correct").val('');
|
||||
}
|
||||
});
|
||||
$("#get-help").click(function(e){
|
||||
e.preventDefault();
|
||||
$("#help-area").text(sessionStorage.getItem('answer'));
|
||||
});
|
||||
$("#api-refresh").click(function(e){
|
||||
e.preventDefault();
|
||||
$.post("{{ url_for('refresh_api') }}")
|
||||
});
|
||||
</script>
|
||||
</html>
|
52
templates/welcome.html
Normal file
52
templates/welcome.html
Normal file
@ -0,0 +1,52 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title></title>
|
||||
<!-- Latest compiled and minified CSS -->
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
|
||||
|
||||
<!-- Optional theme -->
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap-theme.min.css">
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
|
||||
<!-- Latest compiled and minified JavaScript -->
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container centered">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<img class="padded" src="{{ url_for('static', filename='burned.png')}}">
|
||||
</div>
|
||||
<form id="submissionform" role="form" class="col-md-8 form-group" method="POST" action="">
|
||||
<div class="input-group">
|
||||
{{ form.csrf_token }}
|
||||
{{ form.api_key(**{'class':'form-control','placeholder':'Insert API Key'}) }}
|
||||
<span class="input-group-btn">
|
||||
<button id="submitbutton" class="btn btn-default" type="submit">Go!</button>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
<div class="col-md-8">
|
||||
{% for error in form.api_key.errors %}
|
||||
<div class="alert alert-warning alert-dismissible" role="alert">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
{{ error }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-warning alert-dismissible" role="alert">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<strong>{{ category }}</strong>{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
195
wanikaniburned.py
Normal file
195
wanikaniburned.py
Normal file
@ -0,0 +1,195 @@
|
||||
import json
|
||||
import random
|
||||
from flask import Flask, render_template, redirect, url_for, jsonify, flash
|
||||
from flask.ext.login import LoginManager, login_user, login_required, current_user, logout_user, login_fresh
|
||||
from flask.ext.sqlalchemy import SQLAlchemy
|
||||
from flask.ext.wtf import Form
|
||||
import requests
|
||||
from wtforms import StringField
|
||||
from wtforms.validators import DataRequired
|
||||
from datetime import datetime
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config.from_pyfile('config.py')
|
||||
login_manager = LoginManager()
|
||||
login_manager.init_app(app)
|
||||
db = SQLAlchemy(app)
|
||||
|
||||
|
||||
class LoginForm(Form):
|
||||
api_key = StringField('api_key', validators=[DataRequired()])
|
||||
|
||||
|
||||
class User(db.Model):
|
||||
__tablename__ = 'users'
|
||||
api_key = db.Column(db.String, unique=True, primary_key=True)
|
||||
radicals = db.Column(db.String)
|
||||
kanji = db.Column(db.String)
|
||||
vocabulary = db.Column(db.String)
|
||||
username = db.Column(db.String)
|
||||
gravatar = db.Column(db.String)
|
||||
last_updated = db.Column(db.DateTime)
|
||||
|
||||
def __init__(self, api_key):
|
||||
self.api_key = api_key
|
||||
self.last_updated = datetime.utcnow()
|
||||
self.parse_radicals_and_userdata()
|
||||
self.parse_kanji()
|
||||
self.parse_vocabulary()
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
|
||||
def is_authenticated(self):
|
||||
return True
|
||||
|
||||
def is_active(self):
|
||||
return True
|
||||
|
||||
def is_anonymous(self):
|
||||
return False
|
||||
|
||||
def get_id(self):
|
||||
return str(self.api_key)
|
||||
|
||||
def parse_radicals_and_userdata(self):
|
||||
response = requests.get("https://www.wanikani.com/api/user/" + self.api_key + "/radicals/")
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
if data.get('error'):
|
||||
raise ValueError(data['error']['message'])
|
||||
items = []
|
||||
self.username = data['user_information']['username']
|
||||
self.gravatar = data['user_information']['gravatar']
|
||||
if data.get('requested_information'):
|
||||
for item in data['requested_information']:
|
||||
if item.get('user_specific') and item['user_specific']['burned']:
|
||||
items.append({'character': item['character'], 'meaning': item['meaning'], 'image': item['image']})
|
||||
self.radicals = json.dumps(items)
|
||||
|
||||
def parse_kanji(self):
|
||||
response = requests.get("https://www.wanikani.com/api/user/" + self.api_key + "/kanji/")
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
if data.get('error'):
|
||||
raise ValueError(data['error']['message'])
|
||||
items = []
|
||||
if data.get('requested_information'):
|
||||
for item in data['requested_information']:
|
||||
if item.get('user_specific') and item['user_specific']['burned']:
|
||||
items.append({'character': item['character'], 'meaning': item['meaning'], 'onyomi': item['onyomi'],
|
||||
'kunyomi': item['kunyomi']})
|
||||
self.kanji = json.dumps(items)
|
||||
|
||||
def parse_vocabulary(self):
|
||||
response = requests.get("https://www.wanikani.com/api/user/" + self.api_key + "/vocabulary/")
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
if data.get('error'):
|
||||
raise ValueError(data['error']['message'])
|
||||
items = []
|
||||
if data.get('requested_information'):
|
||||
for item in data['requested_information']['general']:
|
||||
if item.get('user_specific') and item['user_specific']['burned']:
|
||||
items.append({'character': item['character'], 'meaning': item['meaning'], 'kana': item['kana']})
|
||||
self.vocabulary = json.dumps(items)
|
||||
|
||||
def update_all(self):
|
||||
self.parse_radicals_and_userdata()
|
||||
self.parse_kanji()
|
||||
self.parse_vocabulary()
|
||||
self.last_updated = datetime.utcnow()
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(api_key):
|
||||
return User.query.get(str(api_key))
|
||||
|
||||
|
||||
@app.route('/', methods=('GET', 'POST'))
|
||||
def show_home():
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('show_quiz'))
|
||||
form = LoginForm()
|
||||
if form.validate_on_submit():
|
||||
user = User.query.get(str(form.api_key.data))
|
||||
if user:
|
||||
login_user(user)
|
||||
return redirect(url_for('show_quiz'))
|
||||
else:
|
||||
try:
|
||||
new_user = User(str(form.api_key.data))
|
||||
login_user(new_user)
|
||||
return redirect(url_for('show_quiz'))
|
||||
except ValueError as err:
|
||||
flash(err)
|
||||
return render_template("welcome.html", form=form)
|
||||
return render_template("welcome.html", form=form)
|
||||
|
||||
|
||||
@login_required
|
||||
@app.route('/quiz')
|
||||
def show_quiz():
|
||||
if not login_fresh():
|
||||
if User.query.get(current_user.api_key):
|
||||
return render_template("quiz.html")
|
||||
else:
|
||||
logout_user()
|
||||
return redirect(url_for('show_home'))
|
||||
return render_template("quiz.html")
|
||||
|
||||
|
||||
@app.route('/quiz_item')
|
||||
@login_required
|
||||
def get_quiz():
|
||||
choices = []
|
||||
if current_user.radicals:
|
||||
choices.append('radical')
|
||||
if current_user.kanji:
|
||||
choices.append('kanji')
|
||||
if current_user.vocabulary:
|
||||
choices.append('vocabulary')
|
||||
selected_type = random.choice(choices)
|
||||
if selected_type is 'radical':
|
||||
selected_item = random.choice(json.loads(current_user.radicals))
|
||||
if selected_item['image']:
|
||||
return jsonify(item_type='radical', question=selected_item['image'], answer=selected_item['meaning'])
|
||||
else:
|
||||
return jsonify(item_type='radical', question=selected_item['character'], answer=selected_item['meaning'])
|
||||
elif selected_type is 'kanji':
|
||||
selected_item = random.choice(json.loads(current_user.kanji))
|
||||
made_answer = ""
|
||||
if selected_item['onyomi'] and selected_item['kunyomi']:
|
||||
made_answer = selected_item['onyomi'] + ',' + selected_item['kunyomi'].replace('.*', '')
|
||||
elif selected_item['onyomi']:
|
||||
made_answer = selected_item['onyomi']
|
||||
elif selected_item['kunyomi']:
|
||||
made_answer = selected_item['kunyomi'].replace('.*', '')
|
||||
return jsonify(item_type='kanji', question=selected_item['character'],
|
||||
answer=made_answer)
|
||||
else:
|
||||
selected_item = random.choice(json.loads(current_user.vocabulary))
|
||||
return jsonify(item_type='vocabulary', question=selected_item['character'], answer=selected_item['kana'])
|
||||
|
||||
|
||||
@app.route('/refresh', methods=['POST'])
|
||||
def refresh_api():
|
||||
current_user.update_all()
|
||||
return "202"
|
||||
|
||||
|
||||
@app.route('/logout')
|
||||
def logout():
|
||||
logout_user()
|
||||
return redirect(url_for('show_home'))
|
||||
|
||||
|
||||
@login_manager.unauthorized_handler
|
||||
def unauthorized():
|
||||
return redirect(url_for('show_home'))
|
||||
|
||||
if __name__ == '__main__':
|
||||
db.create_all()
|
||||
app.debug = True
|
||||
app.run()
|
Loading…
Reference in New Issue
Block a user