Created webapp for reviewing WaniKani burned items.

This commit is contained in:
neviyn 2015-10-04 02:41:16 +01:00
commit c6b94ee31c
7 changed files with 524 additions and 0 deletions

110
.gitignore vendored Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

39
static/style.css Normal file
View 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

File diff suppressed because one or more lines are too long

127
templates/quiz.html Normal file
View 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
View 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">&times;</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">&times;</span></button>
<strong>{{ category }}</strong>{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
</div>
</div>
</div>
</body>
</html>

195
wanikaniburned.py Normal file
View 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()