C.1 Spaced Repetition Software Source Code

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.

C.1.1 answercard.py

#!/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'})

C.1.2 import.py

#!/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'})

C.1.3 new.py

#!/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})

C.1.4 remove.py

#!/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()

C.1.5 edit.py

#!/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()

C.1.6 list.py

#!/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':[]})

C.1.7 timezone.py

#!/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'})