2015-12-24 00:39:52 +00:00
|
|
|
import ujson as json
|
2015-12-12 00:01:19 +00:00
|
|
|
import re
|
|
|
|
from datetime import datetime, timedelta
|
2016-03-15 20:22:04 +00:00
|
|
|
import logging
|
2016-04-03 16:33:50 +01:00
|
|
|
import sys
|
2015-12-12 00:01:19 +00:00
|
|
|
|
2015-11-21 03:00:17 +00:00
|
|
|
import flask
|
2015-12-24 17:03:54 +00:00
|
|
|
from flask.ext.login import LoginManager, login_user, login_required, current_user, logout_user, login_fresh, \
|
|
|
|
confirm_login
|
2015-10-04 02:41:16 +01:00
|
|
|
from flask.ext.sqlalchemy import SQLAlchemy
|
|
|
|
from flask.ext.wtf import Form
|
|
|
|
import requests
|
|
|
|
from wtforms import StringField
|
|
|
|
from wtforms.validators import DataRequired
|
|
|
|
|
2016-03-15 20:22:04 +00:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
logger.setLevel(logging.INFO)
|
2016-04-03 16:33:50 +01:00
|
|
|
console_handler = logging.StreamHandler(sys.stdout)
|
|
|
|
console_handler.setLevel(logging.DEBUG)
|
2016-03-15 20:22:04 +00:00
|
|
|
handler = logging.FileHandler('wkburned.log')
|
|
|
|
handler.setLevel(logging.WARN)
|
|
|
|
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
|
|
handler.setFormatter(formatter)
|
2016-04-03 16:33:50 +01:00
|
|
|
console_handler.setFormatter(formatter)
|
2016-03-15 20:22:04 +00:00
|
|
|
logger.addHandler(handler)
|
2016-04-03 16:33:50 +01:00
|
|
|
logger.addHandler(console_handler)
|
2016-03-15 20:22:04 +00:00
|
|
|
|
2015-11-21 03:00:17 +00:00
|
|
|
app = flask.Flask(__name__)
|
2016-03-15 20:22:04 +00:00
|
|
|
logger.debug("Configuring application.")
|
2016-01-24 19:01:53 +00:00
|
|
|
app.config.update(
|
|
|
|
PORT=5000,
|
|
|
|
JSON_AS_ASCII=False
|
|
|
|
)
|
|
|
|
app.config.from_pyfile('config.py', silent=True)
|
2016-03-15 20:22:04 +00:00
|
|
|
logger.debug("Setting up login manager.")
|
2015-10-04 02:41:16 +01:00
|
|
|
login_manager = LoginManager()
|
|
|
|
login_manager.init_app(app)
|
2016-03-15 20:22:04 +00:00
|
|
|
logger.debug("Setting up database.")
|
2015-10-04 02:41:16 +01:00
|
|
|
db = SQLAlchemy(app)
|
2015-10-19 23:33:26 +01:00
|
|
|
filter_regex = re.compile('\d+([,-]\d+)*')
|
2016-03-15 20:22:04 +00:00
|
|
|
logger.debug("Setup complete")
|
2015-10-04 02:41:16 +01:00
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
2016-03-15 20:03:01 +00:00
|
|
|
# auto_init and username should only be changed for testing purposes
|
|
|
|
def __init__(self, api_key, auto_init=True, username=None):
|
2016-03-15 20:22:04 +00:00
|
|
|
logger.info("Creating new user with %s", api_key)
|
2015-10-04 02:41:16 +01:00
|
|
|
self.api_key = api_key
|
2016-03-15 20:03:01 +00:00
|
|
|
self.username = username
|
2016-01-24 19:01:53 +00:00
|
|
|
self.last_updated = datetime.min
|
2016-01-26 02:56:44 +00:00
|
|
|
if auto_init: # pragma: no cover
|
2016-01-24 19:01:53 +00:00
|
|
|
self.last_updated = datetime.utcnow()
|
|
|
|
self.parse_radicals_and_userdata()
|
|
|
|
self.parse_kanji()
|
|
|
|
self.parse_vocabulary()
|
2016-03-15 20:03:01 +00:00
|
|
|
# If a user with this name already exists, delete it as their api key has probably changed to the new input.
|
|
|
|
if User.query.filter(User.username == self.username).first():
|
2016-03-15 20:22:04 +00:00
|
|
|
logger.info("User with username '%s' already exists, deleting old entry", self.username)
|
2016-03-15 20:03:01 +00:00
|
|
|
db.session.delete(User.query.filter(User.username == self.username).first())
|
2015-10-04 02:41:16 +01:00
|
|
|
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)
|
|
|
|
|
2016-01-26 02:56:44 +00:00
|
|
|
def parse_radicals_and_userdata(self): # pragma: no cover
|
2016-03-15 20:22:04 +00:00
|
|
|
logger.debug("Parsing radicals and userdata for %s", self.api_key)
|
2015-10-04 02:41:16 +01:00
|
|
|
response = requests.get("https://www.wanikani.com/api/user/" + self.api_key + "/radicals/")
|
|
|
|
response.raise_for_status()
|
|
|
|
data = response.json()
|
|
|
|
if data.get('error'):
|
2016-03-15 20:22:04 +00:00
|
|
|
logger.error("Error response from WK when updating radicals and userdata. %s", data['error']['message'])
|
2015-10-04 02:41:16 +01:00
|
|
|
raise ValueError(data['error']['message'])
|
|
|
|
self.username = data['user_information']['username']
|
|
|
|
self.gravatar = data['user_information']['gravatar']
|
|
|
|
if data.get('requested_information'):
|
2015-12-24 00:39:52 +00:00
|
|
|
self.radicals = json.dumps(data['requested_information'], ensure_ascii=False)
|
2015-10-04 02:41:16 +01:00
|
|
|
|
2016-01-26 02:56:44 +00:00
|
|
|
def parse_kanji(self): # pragma: no cover
|
2016-03-15 20:22:04 +00:00
|
|
|
logger.debug("Parsing kanji for %s", self.api_key)
|
2015-10-04 02:41:16 +01:00
|
|
|
response = requests.get("https://www.wanikani.com/api/user/" + self.api_key + "/kanji/")
|
|
|
|
response.raise_for_status()
|
|
|
|
data = response.json()
|
|
|
|
if data.get('error'):
|
2016-03-15 20:22:04 +00:00
|
|
|
logger.error("Error response from WK when updating kanji. %s", data['error']['message'])
|
2015-10-04 02:41:16 +01:00
|
|
|
raise ValueError(data['error']['message'])
|
|
|
|
if data.get('requested_information'):
|
2015-12-24 00:39:52 +00:00
|
|
|
self.kanji = json.dumps(data['requested_information'], ensure_ascii=False)
|
2015-10-04 02:41:16 +01:00
|
|
|
|
2016-01-26 02:56:44 +00:00
|
|
|
def parse_vocabulary(self): # pragma: no cover
|
2016-03-15 20:22:04 +00:00
|
|
|
logger.debug("Parsing vocabulary for %s", self.api_key)
|
2015-10-04 02:41:16 +01:00
|
|
|
response = requests.get("https://www.wanikani.com/api/user/" + self.api_key + "/vocabulary/")
|
|
|
|
response.raise_for_status()
|
|
|
|
data = response.json()
|
|
|
|
if data.get('error'):
|
2016-03-15 20:22:04 +00:00
|
|
|
logger.error("Error response from WK when updating vocabulary. %s", data['error']['message'])
|
2015-10-04 02:41:16 +01:00
|
|
|
raise ValueError(data['error']['message'])
|
|
|
|
if data.get('requested_information'):
|
2015-12-24 00:39:52 +00:00
|
|
|
self.vocabulary = json.dumps(data['requested_information']['general'], ensure_ascii=False)
|
2015-10-04 02:41:16 +01:00
|
|
|
|
2016-01-26 02:56:44 +00:00
|
|
|
def update_all(self): # pragma: no cover
|
2016-03-15 20:22:04 +00:00
|
|
|
logger.info("Updating all info for %s", self.api_key)
|
2015-10-06 00:13:10 +01:00
|
|
|
if (datetime.utcnow() - self.last_updated) > timedelta(hours=1):
|
2016-04-03 16:21:00 +01:00
|
|
|
try:
|
|
|
|
self.parse_radicals_and_userdata()
|
|
|
|
self.parse_kanji()
|
|
|
|
self.parse_vocabulary()
|
|
|
|
self.last_updated = datetime.utcnow()
|
|
|
|
db.session.commit()
|
|
|
|
except requests.exceptions.SSLError:
|
|
|
|
logger.error("%s received an SSL error trying to update. Ensure SSL certs are installed.", self.api_key)
|
|
|
|
raise ValueError('Backend SSL Certificate Error, contact administrator.')
|
2015-10-05 23:19:11 +01:00
|
|
|
else:
|
2016-03-15 20:22:04 +00:00
|
|
|
logger.warn("%s tried to update too frequently", self.api_key)
|
2015-10-05 23:19:11 +01:00
|
|
|
raise ValueError('Cannot refresh now, try again later.')
|
2015-10-04 02:41:16 +01:00
|
|
|
|
|
|
|
|
2015-10-18 00:12:12 +01:00
|
|
|
def parse_range(input_range):
|
2015-10-19 23:33:26 +01:00
|
|
|
if filter_regex.match(input_range):
|
2015-10-18 00:12:12 +01:00
|
|
|
result = []
|
|
|
|
components = input_range.split(',')
|
|
|
|
for item in components:
|
|
|
|
if item.isdigit():
|
2015-10-19 23:33:26 +01:00
|
|
|
result.append(int(item))
|
2015-10-18 00:12:12 +01:00
|
|
|
else:
|
|
|
|
range_ends = item.split('-')
|
2015-11-21 03:00:17 +00:00
|
|
|
result.extend(list(range(int(range_ends[0]), int(range_ends[1]) + 1)))
|
2015-10-18 00:12:12 +01:00
|
|
|
return result
|
|
|
|
|
|
|
|
|
2015-12-10 15:57:52 +00:00
|
|
|
def get_items_with_level_restriction(level_range, item_state, item_types):
|
2015-10-18 00:12:12 +01:00
|
|
|
items = []
|
|
|
|
radical_count = 0
|
2015-12-12 00:01:19 +00:00
|
|
|
loads = json.loads
|
2016-01-24 19:01:53 +00:00
|
|
|
if 'radical' in item_types and current_user.radicals:
|
2015-12-12 00:01:19 +00:00
|
|
|
items.extend({'item_type': 'radical', 'answer': item['meaning'], 'question': item['image']
|
2015-12-24 17:03:54 +00:00
|
|
|
if item['image'] else item['character']} for item in loads(current_user.radicals)
|
2015-12-12 00:01:19 +00:00
|
|
|
if item['user_specific'] and item['user_specific']['srs'] in item_state
|
|
|
|
and item['level'] in level_range)
|
|
|
|
radical_count = len(items)
|
2015-10-18 00:12:12 +01:00
|
|
|
kanji_count = 0
|
2016-01-24 19:01:53 +00:00
|
|
|
if 'kanji' in item_types and current_user.kanji:
|
2015-12-23 23:16:26 +00:00
|
|
|
for item in filter((lambda x: x['user_specific'] and x['user_specific']['srs'] in item_state
|
|
|
|
and x['level'] in level_range), loads(current_user.kanji)):
|
|
|
|
items.extend([{'item_type': 'kanji', 'question': item['character'],
|
2016-01-25 16:48:41 +00:00
|
|
|
'answer': item['onyomi'] if item['important_reading'] == 'onyomi' else item['kunyomi'],
|
2015-12-23 23:16:26 +00:00
|
|
|
'answer_type': 'kana'},
|
|
|
|
{'item_type': 'kanji', 'question': item['character'], 'answer': item['meaning'],
|
2015-12-23 23:40:33 +00:00
|
|
|
'answer_type': 'eng'}])
|
|
|
|
kanji_count = int((len(items) - radical_count) / 2)
|
2015-10-18 00:12:12 +01:00
|
|
|
vocabulary_count = 0
|
2016-01-24 19:01:53 +00:00
|
|
|
if 'vocab' in item_types and current_user.vocabulary:
|
2015-12-23 23:16:26 +00:00
|
|
|
for item in filter((lambda x: x['user_specific'] and x['user_specific']['srs'] in item_state
|
|
|
|
and x['level'] in level_range), loads(current_user.vocabulary)):
|
|
|
|
items.extend([{'item_type': 'vocabulary', 'question': item['character'], 'answer': item['kana'],
|
|
|
|
'answer_type': 'kana'},
|
|
|
|
{'item_type': 'vocabulary', 'question': item['character'],
|
2015-12-23 23:40:33 +00:00
|
|
|
'answer': item['meaning'], 'answer_type': 'eng'}])
|
2016-01-25 21:45:09 +00:00
|
|
|
vocabulary_count = int((len(items) - (radical_count + (kanji_count * 2))) / 2)
|
2015-10-19 23:33:26 +01:00
|
|
|
if not items:
|
2015-11-21 03:00:17 +00:00
|
|
|
return flask.jsonify(error="No items within these filter parameters")
|
2015-10-19 23:33:26 +01:00
|
|
|
else:
|
2015-11-21 03:00:17 +00:00
|
|
|
return flask.jsonify(radical_count=radical_count, kanji_count=kanji_count, vocabulary_count=vocabulary_count,
|
|
|
|
item_list=items)
|
2015-10-18 00:12:12 +01:00
|
|
|
|
|
|
|
|
2015-10-04 02:41:16 +01:00
|
|
|
@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:
|
2015-11-21 03:00:17 +00:00
|
|
|
return flask.redirect(flask.url_for('show_quiz'))
|
2015-10-04 02:41:16 +01:00
|
|
|
form = LoginForm()
|
|
|
|
if form.validate_on_submit():
|
2016-03-15 20:03:01 +00:00
|
|
|
input_data = str(form.api_key.data)
|
|
|
|
user = User.query.get(input_data)
|
|
|
|
if not user:
|
|
|
|
user = User.query.filter(User.username == input_data).first()
|
2015-10-04 02:41:16 +01:00
|
|
|
if user:
|
2015-12-24 17:03:54 +00:00
|
|
|
login_user(user, remember=True)
|
2015-11-21 03:00:17 +00:00
|
|
|
return flask.redirect(flask.url_for('show_quiz'))
|
2016-03-15 20:03:01 +00:00
|
|
|
elif len(input_data) == 32: # pragma: no cover
|
2015-10-04 02:41:16 +01:00
|
|
|
try:
|
2016-03-15 20:03:01 +00:00
|
|
|
new_user = User(input_data)
|
2015-12-24 17:03:54 +00:00
|
|
|
login_user(new_user, remember=True)
|
2015-11-21 03:00:17 +00:00
|
|
|
return flask.redirect(flask.url_for('show_quiz'))
|
2015-10-04 02:41:16 +01:00
|
|
|
except ValueError as err:
|
2015-11-21 03:00:17 +00:00
|
|
|
flask.flash(err)
|
|
|
|
return flask.render_template("welcome.html", form=form)
|
2016-03-15 20:03:01 +00:00
|
|
|
else:
|
|
|
|
flask.flash("API Key length invalid, or username not already in database.")
|
|
|
|
return flask.render_template("welcome.html", form=form)
|
2015-11-21 03:00:17 +00:00
|
|
|
return flask.render_template("welcome.html", form=form)
|
2015-10-04 02:41:16 +01:00
|
|
|
|
|
|
|
|
|
|
|
@login_required
|
|
|
|
@app.route('/quiz')
|
|
|
|
def show_quiz():
|
|
|
|
if not login_fresh():
|
2016-01-24 19:01:53 +00:00
|
|
|
if not current_user.is_anonymous and User.query.get(current_user.api_key):
|
2015-12-24 17:03:54 +00:00
|
|
|
confirm_login()
|
2015-11-21 03:00:17 +00:00
|
|
|
return flask.render_template("quiz.html")
|
2015-10-04 02:41:16 +01:00
|
|
|
else:
|
|
|
|
logout_user()
|
2015-11-21 03:00:17 +00:00
|
|
|
return flask.redirect(flask.url_for('show_home'))
|
|
|
|
return flask.render_template("quiz.html")
|
2015-10-04 02:41:16 +01:00
|
|
|
|
|
|
|
|
2015-10-05 12:25:11 +01:00
|
|
|
@app.route('/user_items')
|
2015-10-04 02:41:16 +01:00
|
|
|
@login_required
|
2015-10-05 12:25:11 +01:00
|
|
|
def get_items():
|
2015-11-17 18:47:00 +00:00
|
|
|
level_range = list(range(0, 61))
|
2015-12-12 00:14:43 +00:00
|
|
|
if flask.request.args.get('level_range') and flask.request.args.get('level_range') is not '':
|
2015-11-21 03:00:17 +00:00
|
|
|
level_range = parse_range(flask.request.args.get('level_range'))
|
2015-11-17 18:58:07 +00:00
|
|
|
item_state = ['burned']
|
2015-12-12 00:14:43 +00:00
|
|
|
if flask.request.args.get('item_state') and flask.request.args.get('item_state') is not '':
|
2015-11-21 03:00:17 +00:00
|
|
|
item_state = flask.request.args.get('item_state').split(',')
|
2015-12-10 15:57:52 +00:00
|
|
|
item_types = ['radical', 'kanji', 'vocab']
|
2015-12-12 00:14:43 +00:00
|
|
|
if flask.request.args.get('item_types') and flask.request.args.get('item_types') is not '':
|
2015-12-10 15:57:52 +00:00
|
|
|
item_types = flask.request.args.get('item_types').split(',')
|
|
|
|
return get_items_with_level_restriction(level_range, item_state, item_types)
|
2015-10-04 02:41:16 +01:00
|
|
|
|
|
|
|
|
|
|
|
@app.route('/refresh', methods=['POST'])
|
2015-12-12 00:01:19 +00:00
|
|
|
@login_required
|
2016-01-26 02:56:44 +00:00
|
|
|
def refresh_api(): # pragma: no cover
|
2015-10-05 23:19:11 +01:00
|
|
|
try:
|
|
|
|
current_user.update_all()
|
2015-12-12 00:01:19 +00:00
|
|
|
return str(datetime_format(current_user.last_updated)), 202
|
2015-10-05 23:19:11 +01:00
|
|
|
except ValueError as err:
|
2016-03-15 20:22:04 +00:00
|
|
|
logger.error("Error during /refresh. %s", str(err))
|
2015-10-05 23:19:11 +01:00
|
|
|
return str(err), 500
|
2015-10-04 02:41:16 +01:00
|
|
|
|
|
|
|
|
|
|
|
@app.route('/logout')
|
|
|
|
def logout():
|
|
|
|
logout_user()
|
2015-11-21 03:00:17 +00:00
|
|
|
return flask.redirect(flask.url_for('show_home'))
|
2015-10-04 02:41:16 +01:00
|
|
|
|
|
|
|
|
|
|
|
@login_manager.unauthorized_handler
|
|
|
|
def unauthorized():
|
2015-11-21 03:00:17 +00:00
|
|
|
return flask.redirect(flask.url_for('show_home'))
|
2015-10-04 02:41:16 +01:00
|
|
|
|
2015-10-22 15:37:07 +01:00
|
|
|
|
2016-01-24 19:01:53 +00:00
|
|
|
@app.template_filter('datetime_format')
|
2015-11-21 03:00:17 +00:00
|
|
|
def datetime_format(input_data):
|
|
|
|
return input_data.strftime("%d %B %Y %I:%M%p")
|
2015-10-22 15:37:07 +01:00
|
|
|
|
|
|
|
|
2015-10-04 02:41:16 +01:00
|
|
|
if __name__ == '__main__':
|
|
|
|
db.create_all()
|
2015-10-05 16:24:30 +01:00
|
|
|
app.run(threaded=True, port=app.config['PORT'])
|