2013-07-13 05:08:16 +08:00
from webob . dec import wsgify
from webob . exc import *
from webob import Response
2013-07-18 00:34:34 +08:00
#from pprint import pprint
2013-07-13 05:08:16 +08:00
try :
import simplejson as json
2013-07-16 00:11:28 +08:00
from simplejson import JSONDecodeError
2013-07-13 05:08:16 +08:00
except ImportError :
import json
2013-07-16 00:11:28 +08:00
JSONDecodeError = ValueError
2013-07-13 05:08:16 +08:00
import os , logging
2013-07-25 03:17:52 +08:00
import anki . consts
2013-07-23 03:11:53 +08:00
import anki . lang
from anki . lang import _ as t
2013-07-17 03:22:17 +08:00
__all__ = [ ' RestApp ' , ' RestHandlerBase ' , ' noReturnValue ' ]
2013-07-13 05:08:16 +08:00
def noReturnValue ( func ) :
func . hasReturnValue = False
return func
class RestHandlerBase ( object ) :
""" Parent class for a handler group. """
hasReturnValue = True
class _RestHandlerWrapper ( RestHandlerBase ) :
2013-07-16 22:12:05 +08:00
""" Wrapper for functions that we can ' t modify. """
2013-07-13 05:08:16 +08:00
def __init__ ( self , func_name , func , hasReturnValue = True ) :
self . func_name = func_name
self . func = func
self . hasReturnValue = hasReturnValue
def __call__ ( self , * args , * * kw ) :
return self . func ( * args , * * kw )
2013-07-23 03:11:53 +08:00
class RestHandlerRequest ( object ) :
2013-07-23 05:37:34 +08:00
def __init__ ( self , app , data , ids , session ) :
self . app = app
2013-07-23 03:11:53 +08:00
self . data = data
self . ids = ids
self . session = session
2013-07-23 05:37:34 +08:00
def copy ( self ) :
return RestHandlerRequest ( self . app , self . data . copy ( ) , self . ids [ : ] , self . session )
def __eq__ ( self , other ) :
return self . app == other . app and self . data == other . data and self . ids == other . ids and self . session == other . session
2013-07-13 05:08:16 +08:00
class RestApp ( object ) :
""" A WSGI app that implements RESTful operations on Collections, Decks and Cards. """
2013-07-16 21:20:31 +08:00
# Defines not only the valid handler types, but their position in the URL string
2013-07-23 06:35:03 +08:00
handler_types = [ ' collection ' , [ ' model ' , ' note ' , ' deck ' , ' card ' ] ]
2013-07-13 05:08:16 +08:00
2013-07-17 02:45:35 +08:00
def __init__ ( self , data_root , allowed_hosts = ' * ' , setup_new_collection = None , use_default_handlers = True , collection_manager = None ) :
2013-07-13 05:08:16 +08:00
from AnkiServer . threading import getCollectionManager
self . data_root = os . path . abspath ( data_root )
self . allowed_hosts = allowed_hosts
2013-07-17 02:45:35 +08:00
self . setup_new_collection = setup_new_collection
2013-07-13 05:08:16 +08:00
if collection_manager is not None :
2013-07-17 02:45:35 +08:00
self . collection_manager = collection_manager
2013-07-13 05:08:16 +08:00
else :
2013-07-17 02:45:35 +08:00
self . collection_manager = getCollectionManager ( )
2013-07-13 05:08:16 +08:00
self . handlers = { }
2013-07-16 21:20:31 +08:00
for type_list in self . handler_types :
if type ( type_list ) is not list :
type_list = [ type_list ]
for handler_type in type_list :
self . handlers [ handler_type ] = { }
2013-07-13 05:08:16 +08:00
if use_default_handlers :
2013-07-16 22:12:05 +08:00
self . add_handler_group ( ' collection ' , CollectionHandler ( ) )
self . add_handler_group ( ' note ' , NoteHandler ( ) )
self . add_handler_group ( ' model ' , ModelHandler ( ) )
self . add_handler_group ( ' deck ' , DeckHandler ( ) )
self . add_handler_group ( ' card ' , CardHandler ( ) )
2013-07-13 05:08:16 +08:00
2013-07-23 03:11:53 +08:00
# hold per collection session data
self . sessions = { }
2013-07-13 05:08:16 +08:00
def add_handler ( self , type , name , handler ) :
""" Adds a callback handler for a type (collection, deck, card) with a unique name.
- ' type ' is the item that will be worked on , for example : collection , deck , and card .
- ' name ' is a unique name for the handler that gets used in the URL .
2013-07-16 22:12:05 +08:00
- ' handler ' is a callable that takes ( collection , data , ids ) .
2013-07-13 05:08:16 +08:00
"""
if self . handlers [ type ] . has_key ( name ) :
raise " Handler already for %(type)s / %(name)s exists! "
self . handlers [ type ] [ name ] = handler
def add_handler_group ( self , type , group ) :
2013-07-16 22:12:05 +08:00
""" Adds several handlers for every public method on an object descended from RestHandlerBase.
2013-07-13 05:08:16 +08:00
This allows you to create a single class with several methods , so that you can quickly
create a group of related handlers . """
import inspect
for name , method in inspect . getmembers ( group , predicate = inspect . ismethod ) :
if not name . startswith ( ' _ ' ) :
if hasattr ( group , ' hasReturnValue ' ) and not hasattr ( method , ' hasReturnValue ' ) :
method = _RestHandlerWrapper ( group . __class__ . __name__ + ' . ' + name , method , group . hasReturnValue )
self . add_handler ( type , name , method )
2013-07-23 05:37:34 +08:00
def execute_handler ( self , type , name , col , req ) :
""" Executes the handler with the given type and name, passing in the col and req as arguments. """
handler , hasReturnValue = self . _getHandler ( type , name )
ret = handler ( col , req )
if hasReturnValue :
return ret
2013-08-01 01:17:58 +08:00
def list_collections ( self ) :
""" Returns an array of valid collection names in our self.data_path. """
return [ x for x in os . listdir ( self . data_root ) if os . path . exists ( os . path . join ( self . data_root , x , ' collection.anki2 ' ) ) ]
2013-07-15 23:13:48 +08:00
def _checkRequest ( self , req ) :
""" Raises an exception if the request isn ' t allowed or valid for some reason. """
2013-07-13 05:08:16 +08:00
if self . allowed_hosts != ' * ' :
try :
remote_addr = req . headers [ ' X-Forwarded-For ' ]
except KeyError :
remote_addr = req . remote_addr
if remote_addr != self . allowed_hosts :
raise HTTPForbidden ( )
2013-07-15 23:13:48 +08:00
2013-07-13 05:08:16 +08:00
if req . method != ' POST ' :
raise HTTPMethodNotAllowed ( allow = [ ' POST ' ] )
2013-07-15 23:13:48 +08:00
def _parsePath ( self , path ) :
""" Takes a request path and returns a tuple containing the handler type, name
and a list of ids .
Raises an HTTPNotFound exception if the path is invalid . """
2013-07-16 00:11:28 +08:00
if path in ( ' ' , ' / ' ) :
raise HTTPNotFound ( )
2013-07-13 05:08:16 +08:00
# split the URL into a list of parts
if path [ 0 ] == ' / ' :
path = path [ 1 : ]
parts = path . split ( ' / ' )
# pull the type and context from the URL parts
2013-07-16 21:20:31 +08:00
handler_type = None
2013-07-13 05:08:16 +08:00
ids = [ ]
2013-07-16 21:20:31 +08:00
for type_list in self . handler_types :
2013-07-16 00:11:28 +08:00
if len ( parts ) == 0 :
break
2013-07-16 21:20:31 +08:00
# some URL positions can have multiple types
if type ( type_list ) is not list :
type_list = [ type_list ]
# get the handler_type
if parts [ 0 ] not in type_list :
2013-07-13 05:08:16 +08:00
break
2013-07-16 21:20:31 +08:00
handler_type = parts . pop ( 0 )
2013-07-16 00:11:28 +08:00
2013-07-16 21:20:31 +08:00
# add the id to the id list
2013-07-13 05:08:16 +08:00
if len ( parts ) > 0 :
ids . append ( parts . pop ( 0 ) )
2013-07-16 21:20:31 +08:00
# break if we don't have enough parts to make a new type/id pair
2013-07-13 05:08:16 +08:00
if len ( parts ) < 2 :
break
2013-07-16 00:11:28 +08:00
2013-07-13 05:08:16 +08:00
# sanity check to make sure the URL is valid
2013-07-16 00:11:28 +08:00
if len ( parts ) > 1 or len ( ids ) == 0 :
2013-07-13 05:08:16 +08:00
raise HTTPNotFound ( )
# get the handler name
if len ( parts ) == 0 :
name = ' index '
else :
name = parts [ 0 ]
2013-07-16 21:20:31 +08:00
return ( handler_type , name , ids )
2013-07-15 23:13:48 +08:00
def _getCollectionPath ( self , collection_id ) :
""" Returns the path to the collection based on the collection_id from the request.
Raises HTTPBadRequest if the collection_id is invalid . """
path = os . path . normpath ( os . path . join ( self . data_root , collection_id , ' collection.anki2 ' ) )
if path [ 0 : len ( self . data_root ) ] != self . data_root :
# attempting to escape our data jail!
raise HTTPBadRequest ( ' " %s " is not a valid collection ' % collection_id )
return path
def _getHandler ( self , type , name ) :
""" Returns a tuple containing handler function for this type and name, and a boolean flag
if that handler has a return value .
Raises an HTTPNotFound exception if the handler doesn ' t exist. " " "
2013-07-13 05:08:16 +08:00
# get the handler function
try :
handler = self . handlers [ type ] [ name ]
except KeyError :
raise HTTPNotFound ( )
# get if we have a return value
hasReturnValue = True
if hasattr ( handler , ' hasReturnValue ' ) :
hasReturnValue = handler . hasReturnValue
2013-07-15 23:13:48 +08:00
return ( handler , hasReturnValue )
def _parseRequestBody ( self , req ) :
""" Parses the request body (JSON) into a Python dict and returns it.
Raises an HTTPBadRequest exception if the request isn ' t valid JSON. " " "
2013-07-13 05:08:16 +08:00
try :
data = json . loads ( req . body )
2013-07-16 00:11:28 +08:00
except JSONDecodeError , e :
2013-07-13 05:08:16 +08:00
logging . error ( req . path + ' : Unable to parse JSON: ' + str ( e ) , exc_info = True )
raise HTTPBadRequest ( )
2013-07-16 00:11:28 +08:00
2013-08-02 00:29:19 +08:00
# fix for a JSON encoding 'quirk' in PHP
if type ( data ) == list and len ( data ) == 0 :
data = { }
2013-07-13 05:08:16 +08:00
# make the keys into non-unicode strings
data = dict ( [ ( str ( k ) , v ) for k , v in data . items ( ) ] )
2013-07-15 23:13:48 +08:00
return data
@wsgify
def __call__ ( self , req ) :
# make sure the request is valid
self . _checkRequest ( req )
2013-08-01 01:17:58 +08:00
if req . path == ' /list_collections ' :
return Response ( json . dumps ( self . list_collections ( ) ) , content_type = ' application/json ' )
2013-07-15 23:13:48 +08:00
# parse the path
type , name , ids = self . _parsePath ( req . path )
# get the collection path
collection_path = self . _getCollectionPath ( ids [ 0 ] )
print collection_path
# get the handler function
handler , hasReturnValue = self . _getHandler ( type , name )
# parse the request body
data = self . _parseRequestBody ( req )
2013-07-23 03:11:53 +08:00
# get the users session
try :
session = self . sessions [ ids [ 0 ] ]
except KeyError :
session = self . sessions [ ids [ 0 ] ] = { }
2013-07-13 05:08:16 +08:00
# debug
from pprint import pprint
pprint ( data )
# run it!
2013-07-17 02:45:35 +08:00
col = self . collection_manager . get_collection ( collection_path , self . setup_new_collection )
2013-07-23 05:37:34 +08:00
handler_request = RestHandlerRequest ( self , data , ids , session )
2013-07-13 05:08:16 +08:00
try :
2013-07-23 03:11:53 +08:00
output = col . execute ( handler , [ handler_request ] , { } , hasReturnValue )
2013-07-13 05:08:16 +08:00
except Exception , e :
logging . error ( e )
return HTTPInternalServerError ( )
if output is None :
return Response ( ' ' , content_type = ' text/plain ' )
else :
return Response ( json . dumps ( output ) , content_type = ' application/json ' )
2013-07-16 22:12:05 +08:00
class CollectionHandler ( RestHandlerBase ) :
2013-07-13 05:08:16 +08:00
""" Default handler group for ' collection ' type. """
2013-07-16 22:12:05 +08:00
#
# MODELS - Store fields definitions and templates for notes
#
2013-07-23 03:11:53 +08:00
def list_models ( self , col , req ) :
2013-07-16 22:12:05 +08:00
# This is already a list of dicts, so it doesn't need to be serialized
return col . models . all ( )
2013-07-23 03:11:53 +08:00
def find_model_by_name ( self , col , req ) :
2013-07-16 22:12:05 +08:00
# This is already a list of dicts, so it doesn't need to be serialized
2013-07-23 03:11:53 +08:00
return col . models . byName ( req . data [ ' model ' ] )
2013-07-16 22:12:05 +08:00
#
# NOTES - Information (in fields per the model) that can generate a card
# (based on a template from the model).
#
2013-07-23 03:11:53 +08:00
def find_notes ( self , col , req ) :
query = req . data . get ( ' query ' , ' ' )
2013-07-16 22:12:05 +08:00
ids = col . findNotes ( query )
2013-07-23 03:11:53 +08:00
if req . data . get ( ' preload ' , False ) :
2013-08-01 03:17:26 +08:00
notes = [ NoteHandler . _serialize ( col . getNote ( id ) ) for id in ids ]
2013-07-16 22:12:05 +08:00
else :
2013-08-01 03:17:26 +08:00
notes = [ { ' id ' : id } for id in ids ]
2013-07-16 22:12:05 +08:00
2013-08-01 03:17:26 +08:00
return notes
def latest_notes ( self , col , req ) :
# TODO: use SQLAlchemy objects to do this
sql = " SELECT n.id FROM notes AS n " ;
args = [ ]
if req . data . has_key ( ' updated_since ' ) :
sql + = ' WHERE n.mod > ? '
args . append ( req . data [ ' updated_since ' ] )
sql + = ' ORDER BY n.mod DESC '
sql + = ' LIMIT ' + str ( req . data . get ( ' limit ' , 10 ) )
ids = col . db . list ( sql , * args )
if req . data . get ( ' preload ' , False ) :
notes = [ NoteHandler . _serialize ( col . getNote ( id ) ) for id in ids ]
else :
notes = [ { ' id ' : id } for id in ids ]
return notes
2013-07-16 22:12:05 +08:00
2013-07-16 23:58:04 +08:00
@noReturnValue
2013-07-23 03:11:53 +08:00
def add_note ( self , col , req ) :
2013-07-16 22:12:05 +08:00
from anki . notes import Note
2013-07-16 23:58:04 +08:00
# TODO: I think this would be better with 'model' for the name
# and 'mid' for the model id.
2013-07-23 03:11:53 +08:00
if type ( req . data [ ' model ' ] ) in ( str , unicode ) :
model = col . models . byName ( req . data [ ' model ' ] )
2013-07-16 22:12:05 +08:00
else :
2013-07-23 03:11:53 +08:00
model = col . models . get ( req . data [ ' model ' ] )
2013-07-16 22:12:05 +08:00
note = Note ( col , model )
2013-07-23 03:11:53 +08:00
for name , value in req . data [ ' fields ' ] . items ( ) :
2013-07-16 22:12:05 +08:00
note [ name ] = value
2013-07-23 03:11:53 +08:00
if req . data . has_key ( ' tags ' ) :
note . setTagsFromStr ( req . data [ ' tags ' ] )
2013-07-16 22:12:05 +08:00
col . addNote ( note )
2013-07-25 02:28:24 +08:00
def list_tags ( self , col , req ) :
return col . tags . all ( )
2013-07-16 22:12:05 +08:00
#
# DECKS - Groups of cards
#
2013-07-13 05:08:16 +08:00
2013-07-23 03:11:53 +08:00
def list_decks ( self , col , req ) :
2013-07-16 22:12:05 +08:00
# This is already a list of dicts, so it doesn't need to be serialized
2013-07-13 05:08:16 +08:00
return col . decks . all ( )
@noReturnValue
2013-07-23 03:11:53 +08:00
def select_deck ( self , col , req ) :
col . decks . select ( req . data [ ' deck_id ' ] )
2013-07-13 05:08:16 +08:00
2013-07-25 03:17:52 +08:00
dyn_modes = {
' random ' : anki . consts . DYN_RANDOM ,
' added ' : anki . consts . DYN_ADDED ,
' due ' : anki . consts . DYN_DUE ,
}
def create_dynamic_deck ( self , col , req ) :
name = req . data . get ( ' name ' , t ( ' Custom Study Session ' ) )
deck = col . decks . byName ( name )
if deck :
if not deck [ ' dyn ' ] :
raise HTTPBadRequest ( " There is an existing non-dynamic deck with the name %s " % name )
# safe to empty it because it's a dynamic deck
# TODO: maybe this should be an option?
col . sched . emptyDyn ( deck [ ' id ' ] )
else :
deck = col . decks . get ( col . decks . newDyn ( name ) )
query = req . data . get ( ' query ' , ' ' )
2013-07-25 04:29:26 +08:00
count = int ( req . data . get ( ' count ' , 100 ) )
2013-07-25 03:17:52 +08:00
mode = req . data . get ( ' mode ' , ' random ' )
try :
mode = self . dyn_modes [ mode ]
except KeyError :
raise HTTPBadRequest ( " Unknown mode: %s " % mode )
deck [ ' terms ' ] [ 0 ] = [ query , count , mode ]
if mode != anki . consts . DYN_RANDOM :
deck [ ' resched ' ] = True
else :
deck [ ' resched ' ] = False
if not col . sched . rebuildDyn ( deck [ ' id ' ] ) :
raise HTTPBadRequest ( " No cards matched the criteria you provided " )
col . decks . save ( deck )
2013-07-25 04:29:26 +08:00
col . sched . reset ( )
2013-07-25 03:17:52 +08:00
return deck
2013-07-16 22:12:05 +08:00
#
# CARD - A specific card in a deck with a history of review (generated from
# a note based on the template).
#
2013-07-23 03:11:53 +08:00
def find_cards ( self , col , req ) :
query = req . data . get ( ' query ' , ' ' )
2013-07-27 02:16:26 +08:00
order = req . data . get ( ' order ' , False )
ids = anki . find . Finder ( col ) . findCards ( query , order )
2013-07-16 22:12:05 +08:00
2013-07-23 03:11:53 +08:00
if req . data . get ( ' preload ' , False ) :
2013-07-26 21:09:25 +08:00
cards = [ CardHandler . _serialize ( col . getCard ( id ) , req . data ) for id in ids ]
2013-07-16 22:12:05 +08:00
else :
2013-07-26 21:09:25 +08:00
cards = [ { ' id ' : id } for id in ids ]
2013-07-16 22:12:05 +08:00
return cards
2013-08-01 03:24:59 +08:00
def latest_cards ( self , col , req ) :
# TODO: use SQLAlchemy objects to do this
sql = " SELECT c.id FROM notes AS n INNER JOIN cards AS c ON c.nid = n.id " ;
args = [ ]
if req . data . has_key ( ' updated_since ' ) :
sql + = ' WHERE n.mod > ? '
args . append ( req . data [ ' updated_since ' ] )
sql + = ' ORDER BY n.mod DESC '
sql + = ' LIMIT ' + str ( req . data . get ( ' limit ' , 10 ) )
ids = col . db . list ( sql , * args )
if req . data . get ( ' preload ' , False ) :
2013-08-01 03:31:48 +08:00
cards = [ CardHandler . _serialize ( col . getCard ( id ) , req . data ) for id in ids ]
2013-08-01 03:24:59 +08:00
else :
cards = [ { ' id ' : id } for id in ids ]
return cards
2013-07-16 22:12:05 +08:00
#
# SCHEDULER - Controls card review, ie. intervals, what cards are due, answering a card, etc.
#
2013-07-23 03:11:53 +08:00
def reset_scheduler ( self , col , req ) :
2013-07-25 00:06:08 +08:00
if req . data . has_key ( ' deck ' ) :
deck = DeckHandler . _get_deck ( col , req . data [ ' deck ' ] )
col . decks . select ( deck [ ' id ' ] )
2013-07-16 21:20:31 +08:00
col . sched . reset ( )
2013-07-25 00:06:08 +08:00
counts = col . sched . counts ( )
return {
' new_cards ' : counts [ 0 ] ,
' learning_cards ' : counts [ 1 ] ,
' review_cards ' : counts [ 1 ] ,
}
def extend_scheduler_limits ( self , col , req ) :
new_cards = int ( req . data . get ( ' new_cards ' , 0 ) )
review_cards = int ( req . data . get ( ' review_cards ' , 0 ) )
col . sched . extendLimits ( new_cards , review_cards )
2013-07-25 00:15:59 +08:00
col . sched . reset ( )
2013-07-16 21:20:31 +08:00
2013-07-23 05:37:34 +08:00
button_labels = [ ' Easy ' , ' Good ' , ' Hard ' ]
def _get_answer_buttons ( self , col , card ) :
l = [ ]
# Put the correct number of buttons
cnt = col . sched . answerButtons ( card )
for idx in range ( 0 , cnt - 1 ) :
l . append ( self . button_labels [ idx ] )
l . append ( ' Again ' )
l . reverse ( )
# Loop through and add the ease, estimated time (in seconds) and other info
return [ {
' ease ' : ease ,
' label ' : label ,
' string_label ' : t ( label ) ,
' interval ' : col . sched . nextIvl ( card , ease ) ,
' string_interval ' : col . sched . nextIvlStr ( card , ease ) ,
} for ease , label in enumerate ( l , 1 ) ]
def next_card ( self , col , req ) :
if req . data . has_key ( ' deck ' ) :
deck = DeckHandler . _get_deck ( col , req . data [ ' deck ' ] )
col . decks . select ( deck [ ' id ' ] )
card = col . sched . getCard ( )
if card is None :
return None
# put it into the card cache to be removed when we answer it
#if not req.session.has_key('cards'):
# req.session['cards'] = {}
#req.session['cards'][long(card.id)] = card
card . startTimer ( )
2013-07-23 06:35:03 +08:00
result = CardHandler . _serialize ( card , req . data )
2013-07-23 05:37:34 +08:00
result [ ' answer_buttons ' ] = self . _get_answer_buttons ( col , card )
return result
@noReturnValue
2013-07-23 03:11:53 +08:00
def answer_card ( self , col , req ) :
import time
card_id = long ( req . data [ ' id ' ] )
ease = int ( req . data [ ' ease ' ] )
card = col . getCard ( card_id )
if card . timerStarted is None :
card . timerStarted = float ( req . data . get ( ' timeStarted ' , time . time ( ) ) )
col . sched . answerCard ( card , ease )
2013-07-23 07:33:53 +08:00
@noReturnValue
def suspend_cards ( self , col , req ) :
card_ids = req . data [ ' ids ' ]
col . sched . suspendCards ( card_ids )
@noReturnValue
def unsuspend_cards ( self , col , req ) :
card_ids = req . data [ ' ids ' ]
col . sched . unsuspendCards ( card_ids )
2013-07-27 20:41:20 +08:00
def cards_recent_ease ( self , col , req ) :
""" Returns the most recent ease for each card. """
# TODO: Use sqlalchemy to build this query!
sql = " SELECT r.cid, r.ease, r.id FROM revlog AS r INNER JOIN (SELECT cid, MAX(id) AS id FROM revlog GROUP BY cid) AS q ON r.cid = q.cid AND r.id = q.id "
where = [ ]
if req . data . has_key ( ' ids ' ) :
where . append ( ' ids IN ( ' + ( ' , ' . join ( [ " ' %s ' " % x for x in req . data [ ' ids ' ] ] ) ) + ' ) ' )
if len ( where ) > 0 :
sql + = ' WHERE ' + ' AND ' . join ( where )
result = [ ]
for r in col . db . all ( sql ) :
result . append ( { ' id ' : r [ 0 ] , ' ease ' : r [ 1 ] , ' timestamp ' : int ( r [ 2 ] / 1000 ) } )
return result
2013-08-02 00:04:14 +08:00
def latest_revlog ( self , col , req ) :
""" Returns recent entries from the revlog. """
# TODO: Use sqlalchemy to build this query!
sql = " SELECT r.id, r.ease, r.cid, r.usn, r.ivl, r.lastIvl, r.factor, r.time, r.type FROM revlog AS r "
args = [ ]
if req . data . has_key ( ' updated_since ' ) :
sql + = ' WHERE r.id > ? '
2013-08-02 00:29:19 +08:00
args . append ( long ( req . data [ ' updated_since ' ] ) * 1000 )
2013-08-02 00:04:14 +08:00
sql + = ' ORDER BY r.id DESC '
sql + = ' LIMIT ' + str ( req . data . get ( ' limit ' , 100 ) )
revlog = col . db . all ( sql , * args )
return [ {
' id ' : r [ 0 ] ,
' ease ' : r [ 1 ] ,
' timestamp ' : int ( r [ 0 ] / 1000 ) ,
' card_id ' : r [ 2 ] ,
' usn ' : r [ 3 ] ,
' interval ' : r [ 4 ] ,
' last_interval ' : r [ 5 ] ,
' factor ' : r [ 6 ] ,
' time ' : r [ 7 ] ,
' type ' : r [ 8 ] ,
} for r in revlog ]
2013-07-27 03:16:26 +08:00
stats_reports = {
' today ' : ' todayStats ' ,
' due ' : ' dueGraph ' ,
' reps ' : ' repsGraph ' ,
' interval ' : ' ivlGraph ' ,
' hourly ' : ' hourGraph ' ,
' ease ' : ' easeGraph ' ,
' card ' : ' cardGraph ' ,
' footer ' : ' footer ' ,
}
stats_reports_order = [ ' today ' , ' due ' , ' reps ' , ' interval ' , ' hourly ' , ' ease ' , ' card ' , ' footer ' ]
def stats_report ( self , col , req ) :
import anki . stats
2013-07-27 03:43:03 +08:00
import re
2013-07-27 03:16:26 +08:00
stats = anki . stats . CollectionStats ( col )
2013-07-27 03:43:03 +08:00
stats . width = int ( req . data . get ( ' width ' , 600 ) )
stats . height = int ( req . data . get ( ' height ' , 200 ) )
2013-07-27 03:16:26 +08:00
reports = req . data . get ( ' reports ' , self . stats_reports_order )
include_css = req . data . get ( ' include_css ' , False )
include_jquery = req . data . get ( ' include_jquery ' , False )
include_flot = req . data . get ( ' include_flot ' , False )
if include_css :
from anki . statsbg import bg
2013-07-27 03:43:03 +08:00
html = stats . css % bg
2013-07-27 03:16:26 +08:00
else :
html = ' '
for name in reports :
if not self . stats_reports . has_key ( name ) :
raise HTTPBadRequest ( " Unknown report name: %s " % name )
func = getattr ( stats , self . stats_reports [ name ] )
2013-07-27 03:43:03 +08:00
html + = ' <div class= " anki-graph anki-graph- %s " > ' % name
html + = func ( )
html + = ' </div> '
2013-07-27 03:16:26 +08:00
# fix an error in some inline styles
# TODO: submit a patch to Anki!
2013-07-27 03:43:03 +08:00
html = re . sub ( r ' style= " width:([0-9 \ .]+); height:([0-9 \ .]+); " ' , r ' style= " width: \ 1px; height: \ 2px; " ' , html )
2013-07-27 03:52:36 +08:00
html = re . sub ( r ' -webkit-transform: ([^;]+); ' , r ' -webkit-transform: \ 1; -moz-transform: \ 1; -ms-transform: \ 1; -o-transform: \ 1; transform: \ 1; ' , html )
2013-07-27 03:16:26 +08:00
scripts = [ ]
if include_jquery or include_flot :
import anki . js
if include_jquery :
scripts . append ( anki . js . jquery )
if include_flot :
scripts . append ( anki . js . plot )
if len ( scripts ) > 0 :
html = " <script> %s \n </script> " % ' ' . join ( scripts ) + html
return html
2013-07-23 03:11:53 +08:00
#
# GLOBAL / MISC
#
@noReturnValue
def set_language ( self , col , req ) :
anki . lang . setLang ( req . data [ ' code ' ] )
2013-07-16 23:58:04 +08:00
class ImportExportHandler ( RestHandlerBase ) :
""" Handler group for the ' collection ' type, but it ' s not added by default. """
def _get_filedata ( self , data ) :
import urllib2
if data . has_key ( ' data ' ) :
return data [ ' data ' ]
fd = None
try :
fd = urllib2 . urlopen ( data [ ' url ' ] )
filedata = fd . read ( )
finally :
if fd is not None :
fd . close ( )
return filedata
def _get_importer_class ( self , data ) :
filetype = data [ ' filetype ' ]
2013-07-17 03:22:17 +08:00
from AnkiServer . importer import get_importer_class
importer_class = get_importer_class ( filetype )
if importer_class is None :
2013-07-16 23:58:04 +08:00
raise HTTPBadRequest ( " Unknown filetype ' %s ' " % filetype )
2013-07-17 03:22:17 +08:00
return importer_class
2013-07-23 03:11:53 +08:00
def import_file ( self , col , req ) :
2013-07-17 03:22:17 +08:00
import AnkiServer . importer
2013-07-16 23:58:04 +08:00
import tempfile
# get the importer class
2013-07-23 03:11:53 +08:00
importer_class = self . _get_importer_class ( req . data )
2013-07-16 23:58:04 +08:00
# get the file data
2013-07-23 03:11:53 +08:00
filedata = self . _get_filedata ( req . data )
2013-07-16 23:58:04 +08:00
# write the file data to a temporary file
try :
path = None
with tempfile . NamedTemporaryFile ( ' wt ' , delete = False ) as fd :
path = fd . name
fd . write ( filedata )
2013-07-17 03:22:17 +08:00
AnkiServer . importer . import_file ( importer_class , col , path )
2013-07-16 23:58:04 +08:00
finally :
if path is not None :
os . unlink ( path )
2013-07-16 22:12:05 +08:00
class ModelHandler ( RestHandlerBase ) :
""" Default handler group for ' model ' type. """
2013-07-23 03:11:53 +08:00
def field_names ( self , col , req ) :
model = col . models . get ( req . ids [ 1 ] )
2013-07-16 22:12:05 +08:00
if model is None :
raise HTTPNotFound ( )
return col . models . fieldNames ( model )
class NoteHandler ( RestHandlerBase ) :
2013-07-16 21:20:31 +08:00
""" Default handler group for ' note ' type. """
@staticmethod
2013-07-16 22:12:05 +08:00
def _serialize ( note ) :
2013-07-16 21:20:31 +08:00
d = {
' id ' : note . id ,
2013-07-23 06:48:06 +08:00
' guid ' : note . guid ,
2013-07-23 06:35:03 +08:00
' model ' : note . model ( ) ,
2013-07-23 06:48:06 +08:00
' mid ' : note . mid ,
' mod ' : note . mod ,
' scm ' : note . scm ,
' tags ' : note . tags ,
' string_tags ' : ' ' . join ( note . tags ) ,
' fields ' : { } ,
' flags ' : note . flags ,
' usn ' : note . usn ,
2013-07-16 21:20:31 +08:00
}
2013-07-23 06:48:06 +08:00
# add all the fields
for name , value in note . items ( ) :
d [ ' fields ' ] [ name ] = value
2013-07-23 06:35:03 +08:00
2013-07-16 21:20:31 +08:00
return d
2013-07-23 03:11:53 +08:00
def index ( self , col , req ) :
note = col . getNote ( req . ids [ 1 ] )
2013-07-16 22:12:05 +08:00
return self . _serialize ( note )
2013-07-16 21:20:31 +08:00
2013-07-23 07:33:53 +08:00
@noReturnValue
def add_tags ( self , col , req ) :
note = col . getNote ( req . ids [ 1 ] )
for tag in req . data [ ' tags ' ] :
note . addTag ( tag )
note . flush ( )
@noReturnValue
def remove_tags ( self , col , req ) :
note = col . getNote ( req . ids [ 1 ] )
for tag in req . data [ ' tags ' ] :
note . delTag ( tag )
note . flush ( )
2013-07-16 22:12:05 +08:00
class DeckHandler ( RestHandlerBase ) :
2013-07-13 05:08:16 +08:00
""" Default handler group for ' deck ' type. """
2013-07-23 05:37:34 +08:00
@staticmethod
def _get_deck ( col , val ) :
2013-07-18 00:34:34 +08:00
try :
2013-07-23 05:37:34 +08:00
did = long ( val )
2013-07-18 00:34:34 +08:00
deck = col . decks . get ( did , False )
except ValueError :
2013-07-23 05:37:34 +08:00
deck = col . decks . byName ( val )
2013-07-18 00:34:34 +08:00
if deck is None :
2013-07-23 05:37:34 +08:00
raise HTTPNotFound ( ' No deck with id or name: ' + str ( val ) )
2013-07-18 00:34:34 +08:00
return deck
2013-07-25 00:06:08 +08:00
def index ( self , col , req ) :
return self . _get_deck ( col , req . ids [ 1 ] )
2013-07-17 00:44:09 +08:00
2013-07-23 03:11:53 +08:00
def next_card ( self , col , req ) :
2013-07-23 05:37:34 +08:00
req_copy = req . copy ( )
req_copy . data [ ' deck ' ] = req . ids [ 1 ]
del req_copy . ids [ 1 ]
2013-07-23 03:11:53 +08:00
2013-07-23 05:37:34 +08:00
# forward this to the CollectionHandler
return req . app . execute_handler ( ' collection ' , ' next_card ' , col , req_copy )
2013-07-13 05:08:16 +08:00
2013-07-25 00:06:08 +08:00
def get_conf ( self , col , req ) :
# TODO: should probably live in a ConfHandler
return col . decks . confForDid ( req . ids [ 1 ] )
@noReturnValue
def set_update_conf ( self , col , req ) :
data = req . data . copy ( )
del data [ ' id ' ]
conf = col . decks . confForDid ( req . ids [ 1 ] )
conf = conf . copy ( )
conf . update ( data )
col . decks . updateConf ( conf )
2013-07-16 22:12:05 +08:00
class CardHandler ( RestHandlerBase ) :
2013-07-16 21:20:31 +08:00
""" Default handler group for ' card ' type. """
2013-07-13 05:08:16 +08:00
2013-07-16 21:20:31 +08:00
@staticmethod
2013-07-23 06:35:03 +08:00
def _serialize ( card , opts ) :
2013-07-16 21:20:31 +08:00
d = {
2013-07-18 00:34:34 +08:00
' id ' : card . id ,
' isEmpty ' : card . isEmpty ( ) ,
2013-07-26 21:09:25 +08:00
' css ' : card . css ( ) ,
' question ' : card . _getQA ( ) [ ' q ' ] ,
' answer ' : card . _getQA ( ) [ ' a ' ] ,
2013-07-18 00:34:34 +08:00
' did ' : card . did ,
' due ' : card . due ,
' factor ' : card . factor ,
' ivl ' : card . ivl ,
' lapses ' : card . lapses ,
' left ' : card . left ,
' mod ' : card . mod ,
' nid ' : card . nid ,
' odid ' : card . odid ,
' odue ' : card . odue ,
' ord ' : card . ord ,
' queue ' : card . queue ,
' reps ' : card . reps ,
' type ' : card . type ,
' usn ' : card . usn ,
2013-07-23 03:11:53 +08:00
' timerStarted ' : card . timerStarted ,
2013-07-16 21:20:31 +08:00
}
2013-07-23 06:35:03 +08:00
if opts . get ( ' load_note ' , False ) :
d [ ' note ' ] = NoteHandler . _serialize ( card . col . getNote ( card . nid ) )
if opts . get ( ' load_deck ' , False ) :
d [ ' deck ' ] = card . col . decks . get ( card . did )
2013-08-01 03:01:35 +08:00
if opts . get ( ' load_latest_revlog ' , False ) :
d [ ' latest_revlog ' ] = CardHandler . _latest_revlog ( card . col , card . id )
2013-07-16 21:20:31 +08:00
return d
2013-07-13 05:08:16 +08:00
2013-08-01 03:01:35 +08:00
@staticmethod
def _latest_revlog ( col , card_id ) :
r = col . db . first ( " SELECT r.id, r.ease FROM revlog AS r WHERE r.cid = ? ORDER BY id DESC LIMIT 1 " , card_id )
if r :
return { ' id ' : r [ 0 ] , ' ease ' : r [ 1 ] , ' timestamp ' : int ( r [ 0 ] / 1000 ) }
2013-07-23 06:35:03 +08:00
def index ( self , col , req ) :
card = col . getCard ( req . ids [ 1 ] )
return self . _serialize ( card , req . data )
2013-07-26 20:38:50 +08:00
def _forward_to_note ( self , col , req , name ) :
card = col . getCard ( req . ids [ 1 ] )
2013-07-23 07:33:53 +08:00
req_copy = req . copy ( )
req_copy . ids [ 1 ] = card . nid
2013-07-26 20:38:50 +08:00
return req . app . execute_handler ( ' note ' , name , col , req )
2013-07-23 07:33:53 +08:00
@noReturnValue
def add_tags ( self , col , req ) :
2013-07-26 20:38:50 +08:00
self . _forward_to_note ( col , req , ' add_tags ' )
2013-07-23 07:33:53 +08:00
@noReturnValue
def remove_tags ( self , col , req ) :
2013-07-26 20:38:50 +08:00
self . _forward_to_note ( col , req , ' remove_tags ' )
2013-07-23 07:33:53 +08:00
2013-07-27 02:25:42 +08:00
def stats_report ( self , col , req ) :
card = col . getCard ( req . ids [ 1 ] )
return col . cardStats ( card )
2013-08-01 03:01:35 +08:00
def latest_revlog ( self , col , req ) :
return self . _latest_revlog ( col , req . ids [ 1 ] )
2013-07-13 05:08:16 +08:00
# Our entry point
def make_app ( global_conf , * * local_conf ) :
2013-07-23 03:11:53 +08:00
# TODO: we should setup the default language from conf!
2013-07-13 05:08:16 +08:00
# setup the logger
2013-07-17 02:50:45 +08:00
from AnkiServer . utils import setup_logging
2013-07-18 09:17:36 +08:00
setup_logging ( local_conf . get ( ' logging.config_file ' ) )
2013-07-13 05:08:16 +08:00
return RestApp (
data_root = local_conf . get ( ' data_root ' , ' . ' ) ,
allowed_hosts = local_conf . get ( ' allowed_hosts ' , ' * ' )
)