#!/usr/bin/env python3
"""
Flashcard Learning System - Flask API + Web UI
Features:
- Create/manage decks and cards (EN/KO)
- Spaced repetition scheduling
- Study sessions with tracking
- User separation (for future SSO)
"""
from flask import Flask, request, jsonify, render_template_string, session
import os
import json
from datetime import datetime, timedelta
app = Flask(__name__)
app.secret_key = os.environ.get('FLASH_CARD_SECRET', 'dev-secret-change-in-prod')
DATA_DIR = os.environ.get('FLASH_CARD_DATA_DIR', '/home/joungmin/flashcards')
def load_data():
os.makedirs(DATA_DIR, exist_ok=True)
users_file = os.path.join(DATA_DIR, 'users.json')
decks_file = os.path.join(DATA_DIR, 'decks.json')
cards_file = os.path.join(DATA_DIR, 'cards.json')
sessions_file = os.path.join(DATA_DIR, 'sessions.json')
def read_json(f, default):
if os.path.exists(f):
with open(f, 'r') as file:
return json.load(file)
return default
return {
'users': read_json(users_file, {}),
'decks': read_json(decks_file, {}),
'cards': read_json(cards_file, {}),
'sessions': read_json(sessions_file, {})
}
def save_data(data):
users_file = os.path.join(DATA_DIR, 'users.json')
decks_file = os.path.join(DATA_DIR, 'decks.json')
cards_file = os.path.join(DATA_DIR, 'cards.json')
sessions_file = os.path.join(DATA_DIR, 'sessions.json')
with open(users_file, 'w') as f:
json.dump(data['users'], f, indent=2, default=str)
with open(decks_file, 'w') as f:
json.dump(data['decks'], f, indent=2, default=str)
with open(cards_file, 'w') as f:
json.dump(data['cards'], f, indent=2, default=str)
with open(sessions_file, 'w') as f:
json.dump(data['sessions'], f, indent=2, default=str)
def get_next_id(data, key):
if not data[key]:
return 1
return max(int(k) for k in data[key].keys()) + 1
def get_current_user():
return session.get('user_id', 1)
HTML_DECKS = """
Flashcards - EN/KO Learning
My Decks
+ New Deck
{% if decks %}
{% for deck_id, deck in decks.items() %}
{{ deck.description or 'No description' }}
{{ deck.language_pair }} • {{ deck.card_count }} cards
{% endfor %}
{% else %}
No decks yet. Create your first deck!
{% endif %}
"""
HTML_DECK_EDIT = """
{% if deck_id %}Edit{% else %}New{% endif %} Deck
{% if deck_id %}Edit Deck{% else %}New Deck{% endif %}
{% if deck_id %}
Cards ({{ cards|length }})
+ Add Card
{% for card_id, card in cards.items() %}
{{ card.question_en }}
{{ card.answer_en or card.question_ko }}
Edit | Delete
{% endfor %}
{% endif %}
"""
HTML_CARD_EDIT = """
{% if card_id %}Edit{% else %}New{% endif %} Card
{% if card_id %}Edit Card{% else %}New Card{% endif %}
"""
HTML_STUDY = """
Study: {{ deck.name }}
Study: {{ deck.name }}
Total: {{ cards|length }}
Due: {{ due_cards|length }}
Correct: {{ correct }}
Incorrect: {{ incorrect }}
{% if due_cards %}
Question
{{ current_card.question_en }}
{{ deck.language_pair }}
Answer
{{ current_card.answer_en or current_card.question_ko }}
{% else %}
No cards due for review!
Back to Decks
{% endif %}
"""
@app.route('/')
def index():
data = load_data()
user_id = get_current_user()
user_decks = {k: v for k, v in data['decks'].items()
if str(v.get('user_id', 1)) == str(user_id)}
for deck_id in user_decks:
cards = [c for c in data['cards'].values()
if str(c.get('deck_id')) == str(deck_id)]
user_decks[deck_id]['card_count'] = len(cards)
return render_template_string(HTML_DECKS, decks=user_decks)
@app.route('/deck/new', methods=['GET', 'POST'])
def new_deck():
data = load_data()
if request.method == 'POST':
deck_id = get_next_id(data, 'decks')
user_id = get_current_user()
data['decks'][str(deck_id)] = {
'deck_id': deck_id, 'user_id': user_id,
'name': request.form['name'],
'description': request.form.get('description', ''),
'language_pair': request.form.get('language_pair', 'EN-KO'),
'created_at': datetime.now().isoformat(), 'is_active': 'Y'
}
save_data(data)
return redirect(f'/deck/{deck_id}')
return render_template_string(HTML_DECK_EDIT, deck=None, cards={}, deck_id=None)
@app.route('/deck/')
def view_deck(deck_id):
data = load_data()
deck = data['decks'].get(str(deck_id))
if not deck:
return "Deck not found", 404
cards = {k: v for k, v in data['cards'].items()
if str(v.get('deck_id')) == str(deck_id)}
return render_template_string(HTML_DECK_EDIT, deck=deck, cards=cards, deck_id=deck_id)
@app.route('/deck//edit', methods=['POST'])
def edit_deck(deck_id):
data = load_data()
if str(deck_id) in data['decks']:
data['decks'][str(deck_id)].update({
'name': request.form['name'],
'description': request.form.get('description', ''),
'language_pair': request.form.get('language_pair', 'EN-KO'),
'updated_at': datetime.now().isoformat()
})
save_data(data)
return redirect(f'/deck/{deck_id}')
@app.route('/card/new/', methods=['GET', 'POST'])
def new_card(deck_id):
data = load_data()
if request.method == 'POST':
card_id = get_next_id(data, 'cards')
data['cards'][str(card_id)] = {
'card_id': card_id, 'deck_id': int(deck_id),
'question_en': request.form['question_en'],
'answer_en': request.form.get('answer_en', ''),
'question_ko': request.form['question_ko'],
'answer_ko': request.form.get('answer_ko', ''),
'example_sentence': request.form.get('example_sentence', ''),
'difficulty_level': int(request.form.get('difficulty_level', 1)),
'created_at': datetime.now().isoformat(), 'is_active': 'Y'
}
save_data(data)
return redirect(f'/deck/{deck_id}')
return render_template_string(HTML_CARD_EDIT, card=None, card_id=None)
@app.route('/card//edit', methods=['GET', 'POST'])
def edit_card(card_id):
data = load_data()
card = data['cards'].get(str(card_id))
if not card:
return "Card not found", 404
if request.method == 'POST':
card.update({
'question_en': request.form['question_en'],
'answer_en': request.form.get('answer_en', ''),
'question_ko': request.form['question_ko'],
'answer_ko': request.form.get('answer_ko', ''),
'example_sentence': request.form.get('example_sentence', ''),
'difficulty_level': int(request.form.get('difficulty_level', 1)),
'updated_at': datetime.now().isoformat()
})
save_data(data)
return redirect(f"/deck/{card['deck_id']}")
return render_template_string(HTML_CARD_EDIT, card=card, card_id=card_id)
@app.route('/card//delete')
def delete_card(card_id):
data = load_data()
if str(card_id) in data['cards']:
deck_id = data['cards'][str(card_id)]['deck_id']
del data['cards'][str(card_id)]
save_data(data)
return redirect(f'/deck/{deck_id}')
return "Card not found", 404
@app.route('/study/')
def study_deck(deck_id):
data = load_data()
deck = data['decks'].get(str(deck_id))
if not deck:
return "Deck not found", 404
now = datetime.now().isoformat()
cards = [c for c in data['cards'].values()
if str(c.get('deck_id')) == str(deck_id) and c.get('is_active', 'Y') == 'Y']
cards.sort(key=lambda x: (x.get('next_review_at', now), x.get('difficulty_level', 1)))
session_id = get_next_id(data, 'sessions')
data['sessions'][str(session_id)] = {
'session_id': session_id, 'user_id': get_current_user(),
'deck_id': int(deck_id), 'started_at': now,
'cards_reviewed': 0, 'cards_correct': 0, 'cards_incorrect': 0
}
save_data(data)
return render_template_string(
HTML_STUDY, deck=deck, cards=cards,
due_cards=cards[:20], current_card=cards[0] if cards else None,
session_id=session_id, correct=0, incorrect=0
)
# API Routes
@app.route('/api/decks', methods=['GET'])
def api_decks():
data = load_data()
user_id = get_current_user()
user_decks = [v for k, v in data['decks'].items()
if str(v.get('user_id', 1)) == str(user_id)]
return jsonify({'decks': user_decks})
@app.route('/api/decks', methods=['POST'])
def api_create_deck():
data = load_data()
deck_id = get_next_id(data, 'decks')
user_id = get_current_user()
req = request.json
data['decks'][str(deck_id)] = {
'deck_id': deck_id, 'user_id': user_id,
'name': req.get('name'), 'description': req.get('description', ''),
'language_pair': req.get('language_pair', 'EN-KO'),
'created_at': datetime.now().isoformat(), 'is_active': 'Y'
}
save_data(data)
return jsonify({'success': True, 'deck_id': deck_id})
@app.route('/api/cards', methods=['GET'])
def api_cards():
data = load_data()
deck_id = request.args.get('deck_id')
cards = data['cards'].values()
if deck_id:
cards = [c for c in cards if str(c.get('deck_id')) == str(deck_id)]
return jsonify({'cards': list(cards)})
@app.route('/api/cards', methods=['POST'])
def api_create_card():
data = load_data()
card_id = get_next_id(data, 'cards')
req = request.json
data['cards'][str(card_id)] = {
'card_id': card_id, 'deck_id': req.get('deck_id'),
'question_en': req.get('question_en'),
'answer_en': req.get('answer_en', ''),
'question_ko': req.get('question_ko'),
'answer_ko': req.get('answer_ko', ''),
'example_sentence': req.get('example_sentence', ''),
'difficulty_level': req.get('difficulty_level', 1),
'created_at': datetime.now().isoformat(), 'is_active': 'Y'
}
save_data(data)
return jsonify({'success': True, 'card_id': card_id})
@app.route('/api/cards/', methods=['DELETE'])
def api_delete_card(card_id):
data = load_data()
if str(card_id) in data['cards']:
del data['cards'][str(card_id)]
save_data(data)
return jsonify({'success': True})
return jsonify({'error': 'Card not found'}), 404
@app.route('/api/session//result', methods=['POST'])
def api_session_result(session_id):
data = load_data()
if str(session_id) in data['sessions']:
req = request.json
session_data = data['sessions'][str(session_id)]
session_data['cards_reviewed'] = session_data.get('cards_reviewed', 0) + 1
if req.get('was_correct'):
session_data['cards_correct'] = session_data.get('cards_correct', 0) + 1
else:
session_data['cards_incorrect'] = session_data.get('cards_incorrect', 0) + 1
card_id = req.get('card_id')
if str(card_id) in data['cards']:
card = data['cards'][str(card_id)]
card['times_reviewed'] = card.get('times_reviewed', 0) + 1
if req.get('was_correct'):
card['times_correct'] = card.get('times_correct', 0) + 1
card['last_reviewed_at'] = datetime.now().isoformat()
days = card.get('times_correct', 1) if req.get('was_correct') else 0
card['next_review_at'] = (datetime.now() + timedelta(days=days*2)).isoformat()
save_data(data)
return jsonify({'success': True})
return jsonify({'error': 'Session not found'}), 404
if __name__ == '__main__':
port = int(os.environ.get('PORT', 8082))
app.run(host='0.0.0.0', port=port, debug=False)