The software is based on Anki (version $2.0.20$), a popular spaced repetition program. Since Anki is under the gnu agpl license, I’ll provide the scripts I wrote here:
answercard.py | Answer a card and return the next one |
import.py | Import cards (triggered by the ‘Add these questions to my cards’ link) |
new.py | Create new card |
remove.py | Remove a card |
edit.py | Edit a card |
list.py | List all cards |
timezone.py | Change the timezone |
These scripts return json data to the browser, and are called by a php script which just does login stuff and logs errors etc.
#!/usr/local/bin/python2.7
# -*- coding: utf-8 -*-
#If the only argument is the path to the collection file, then send the next question.
#When a question is answered, call this script with the ease, the card id,
#and the start time as well, and it will send the next question back.
import os,time,sys,codecs,json
from anki.storage import Collection
sys.stdout = codecs.getwriter('utf-8')(sys.stdout)
col_file = sys.argv[1]
def make_json(card):
c.sched.reset()
#Find the number of cards remaining to be reviewed today.
#Multiply newCount by 2 because new cards are usually reviewed twice
counts = c.sched.lrnCount + c.sched.revCount + 2*c.sched.newCount
print json.dumps({
'status': 'success',
'q_all': card.note().fields[0],
'a_all': card.note().fields[1],
'reps': card.reps,
'tags': card.note().stringTags(),
'id': card.id,
'time': time.time(),
'counts': counts,
'answerbuttons': c.sched.answerButtons(card)})
if os.path.exists(col_file):
c = Collection(col_file)
#get the next card. if we're answering a card, then they should be the same,
#and this step is necessary to pop it off the queues. but if they're not the
#same then we'll return this card at the end as the next card
card = c.sched.getCard()
#if the user has just answered a card, then tell anki about it
if len(sys.argv) == 5:
ease = int(sys.argv[2])
cid = long(sys.argv[3])
t = float(sys.argv[4])
answered_card = c.getCard(cid)
answered_card.timerStarted = t
c.sched.answerCard(answered_card,ease)
c.save()
#retrieve the next card
if card:
if cid == card.id:
card = c.sched.getCard()
if card:
make_json(card)
elif len(c.db.list("select id from cards where did in (1)")) > 0:
print json.dumps({'status': 'finished'})
else:
print json.dumps({'status': 'no_cards'})
c.close()
else: #the user hasn't added any cards
print json.dumps({'status': 'no_cards'})
#!/usr/local/bin/python2.7
#Import cards from a tab-delimited csv file
import os,sys,json
from anki.storage import Collection
col_file = sys.argv[1]
card_name = sys.argv[2]
card_file = sys.argv[3]
c = Collection(col_file)
from anki.importing.csvfile import TextImporter
t = TextImporter(c,card_file)
t.delimiter = "\t"
t.tagsToAdd = [card_name]
t.initMapping()
t.run()
c.sched.randomizeCards(c.decks.selected())
c.save()
c.close()
print json.dumps({'status':'success'})
#!/usr/local/bin/python2.7
# -*- coding: utf-8 -*-
#Create a new card.
#Variations are separated by '***' eg
#"new.py path_to_collection '2+3=?***3+2=?' '5***5'"
#will create a card with two variations:
#Q:2+3=? A:5
#Q:3+2=? A:5
import sys,os,json
from anki.storage import Collection
col_file = sys.argv[1]
q = sys.argv[2]
a = sys.argv[3]
c = Collection(col_file)
note = c.newNote()
note.fields[0] = unicode(q,'utf_8')
note.fields[1] = unicode(a,'utf_8')
#The following is taken from collection.py's addNote function but with some
#modifications so we can get the card id out to pass back to the browser.
# check we have card models available, then save
cms = c.findTemplates(note)
note.flush()
# deck conf governs which of these are used
due = c.nextID("pos")
# add cards
for template in cms:
card = c._newCard(note, template, due)
c.close()
print json.dumps({'status': 'success', 'cid': card.id})
#!/usr/local/bin/python2.7
#Remove the card with the given card id from the collection
import json,os,sys
col_file = sys.argv[1]
cid = sys.argv[2]
from anki.storage import Collection
c = Collection(col_file)
try:
c.remCards([long(cid)])
print json.dumps({'status':'success'})
except ValueError:
print json.dumps({'status': 'error', 'message': 'Invalid card id'})
c.close()
#!/usr/local/bin/python2.7
#Edit the given card, replacing the old question and answer with the new ones.
#See new.py for syntax of variations.
import sys,os,json
from anki.storage import Collection
col_file = sys.argv[1]
cid = sys.argv[2]
q = sys.argv[3]
a = sys.argv[4]
c = Collection(col_file)
try:
card = c.getCard(long(cid))
note = card.note()
note.fields[0] = unicode(q,'utf_8')
note.fields[1] = unicode(a,'utf_8')
note.flush()
print json.dumps({'status': 'success'})
except TypeError:
print json.dumps({
'status': 'error',
'message': 'Card doesn\'t exist'})
except ValueError:
print json.dumps({
'status': 'error',
'message': 'Invalid card id'})
c.close()
#!/usr/local/bin/python2.7
#List all the cards in the collection. Also return the collection's timezone
import json,os,sys,time
from anki.storage import Collection
col_file = sys.argv[1]
if os.path.exists(col_file):
c = Collection(col_file)
timezone = 4 - time.gmtime(c.crt).tm_hour #find timezone assuming creation time is 4am
if timezone < -11:
timezone += 24
l = c.db.list("select id from cards where did in (1)")
cards = []
for cid in l:
card = c.getCard(cid)
cards.append({
'cid': cid,
'q': card.note().fields[0],
'a': card.note().fields[1],
'reps': card.reps,
'tags': card.note().stringTags()})
print json.dumps({'status':'success',
'cards':cards,
'tomorrow_minutes': int((c.sched.dayCutoff - time.time())/60), #minutes until next learning day starts
'timezone': timezone})
c.close()
else: #return an empty list of cards if the collection hasn't been created
print json.dumps({'status':'success','cards':[]})
#!/usr/local/bin/python2.7
#Change the creation time of the collection to adjust timezone
import json,os,sys,time,calendar
from anki.storage import Collection
col_file = sys.argv[1]
timezone = sys.argv[2]
hours = (4 - int(timezone)) % 24 #new day at hours after midnight GMT (4am in given timezone)
c = Collection(col_file)
t = time.gmtime(c.crt) #collection creation time
c.crt = int(calendar.timegm([t.tm_year,t.tm_mon,t.tm_mday,hours,0,0]))
c.setMod()
c.close()
print json.dumps({'status': 'success'})