diff --git a/AnkiServer/apps/rest_app.py b/AnkiServer/apps/rest_app.py index 9481d25..4d39a5c 100644 --- a/AnkiServer/apps/rest_app.py +++ b/AnkiServer/apps/rest_app.py @@ -23,16 +23,11 @@ def noReturnValue(func): return func class RestHandlerBase(object): - """Parent class for single handler callbacks.""" - hasReturnValue = True - def __call__(self, collection, data, ids): - pass - -class RestHandlerGroupBase(object): """Parent class for a handler group.""" hasReturnValue = True class _RestHandlerWrapper(RestHandlerBase): + """Wrapper for functions that we can't modify.""" def __init__(self, func_name, func, hasReturnValue=True): self.func_name = func_name self.func = func @@ -44,7 +39,9 @@ class RestApp(object): """A WSGI app that implements RESTful operations on Collections, Decks and Cards.""" # Defines not only the valid handler types, but their position in the URL string - handler_types = ['collection', ['deck', 'note'], 'card'] + # TODO: this broken - it allows a model to contain cards, for example.. We need to + # give a pattern for each handler type. + handler_types = ['collection', ['model', 'note', 'deck'], 'card'] def __init__(self, data_root, allowed_hosts='*', use_default_handlers=True, collection_manager=None): from AnkiServer.threading import getCollectionManager @@ -53,9 +50,9 @@ class RestApp(object): self.allowed_hosts = allowed_hosts if collection_manager is not None: - self.collection_manager = collection_manager + col = collection_manager else: - self.collection_manager = getCollectionManager() + col = getCollectionManager() self.handlers = {} for type_list in self.handler_types: @@ -65,10 +62,11 @@ class RestApp(object): self.handlers[handler_type] = {} if use_default_handlers: - self.add_handler_group('collection', CollectionHandlerGroup()) - self.add_handler_group('note', NoteHandlerGroup()) - self.add_handler_group('deck', DeckHandlerGroup()) - self.add_handler_group('card', CardHandlerGroup()) + 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()) def add_handler(self, type, name, handler): """Adds a callback handler for a type (collection, deck, card) with a unique name. @@ -77,7 +75,7 @@ class RestApp(object): - 'name' is a unique name for the handler that gets used in the URL. - - 'handler' handler can be a Python method or a subclass of the RestHandlerBase class. + - 'handler' is a callable that takes (collection, data, ids). """ if self.handlers[type].has_key(name): @@ -85,7 +83,7 @@ class RestApp(object): self.handlers[type][name] = handler def add_handler_group(self, type, group): - """Adds several handlers for every public method on an object descended from RestHandlerGroup. + """Adds several handlers for every public method on an object descended from RestHandlerBase. This allows you to create a single class with several methods, so that you can quickly create a group of related handlers.""" @@ -241,37 +239,117 @@ class RestApp(object): else: return Response(json.dumps(output), content_type='application/json') -class CollectionHandlerGroup(RestHandlerGroupBase): +class CollectionHandler(RestHandlerBase): """Default handler group for 'collection' type.""" + + # + # MODELS - Store fields definitions and templates for notes + # + + def list_models(self, col, data, ids): + # This is already a list of dicts, so it doesn't need to be serialized + return col.models.all() + + def find_model_by_name(self, col, data, ids): + # This is already a list of dicts, so it doesn't need to be serialized + return col.models.byName(data['model']) + + # + # NOTES - Information (in fields per the model) that can generate a card + # (based on a template from the model). + # + + def find_notes(self, col, data, ids): + query = data.get('query', '') + ids = col.findNotes(query) + + if data.get('preload', False): + nodes = [NoteHandler._serialize(col.getNote(id)) for id in ids] + else: + nodes = [{'id': id} for id in ids] + + return nodes + + def add_note(self, col, data, ids): + from anki.notes import Note + + if type(data['model']) in (str, unicode): + model = col.models.byName(data['model']) + else: + model = col.models.get(data['model']) + + note = Note(col, model) + for name, value in data['fields'].items(): + note[name] = value + + if data.has_key('tags'): + note.setTagsFromStr(data['tags']) + + col.addNote(note) + + # + # DECKS - Groups of cards + # def list_decks(self, col, data, ids): + # This is already a list of dicts, so it doesn't need to be serialized return col.decks.all() @noReturnValue def select_deck(self, col, data, ids): col.decks.select(data['deck_id']) + # + # CARD - A specific card in a deck with a history of review (generated from + # a note based on the template). + # + + def find_cards(self, col, data, ids): + query = data.get('query', '') + ids = col.findCards(query) + + if data.get('preload', False): + cards = [CardHandler._serialize(col.getCard(id)) for id in ids] + else: + cards = [{'id': id} for id in ids] + + return cards + + # + # SCHEDULER - Controls card review, ie. intervals, what cards are due, answering a card, etc. + # + @noReturnValue def sched_reset(self, col, data, ids): col.sched.reset() -class NoteHandlerGroup(RestHandlerGroupBase): +class ModelHandler(RestHandlerBase): + """Default handler group for 'model' type.""" + + def field_names(self, col, data, ids): + model = col.models.get(ids[1]) + if model is None: + raise HTTPNotFound() + return col.models.fieldNames(model) + +class NoteHandler(RestHandlerBase): """Default handler group for 'note' type.""" @staticmethod - def _serialize_note(note): + def _serialize(note): d = { 'id': note.id, 'model': note.model()['name'], + 'tags': ' '.join(note.tags), } # TODO: do more stuff! return d def index(self, col, data, ids): note = col.getNote(ids[1]) - return self._serialize_note(note) + return self._serialize(note) -class DeckHandlerGroup(RestHandlerGroupBase): +class DeckHandler(RestHandlerBase): """Default handler group for 'deck' type.""" def next_card(self, col, data, ids): @@ -282,13 +360,13 @@ class DeckHandlerGroup(RestHandlerGroupBase): if card is None: return None - return CardHandlerGroup._serialize_card(card) + return CardHandler._serialize(card) -class CardHandlerGroup(RestHandlerGroupBase): +class CardHandler(RestHandlerBase): """Default handler group for 'card' type.""" @staticmethod - def _serialize_card(card): + def _serialize(card): d = { 'id': card.id } diff --git a/tests/test_rest_app.py b/tests/test_rest_app.py index 77b4320..b6360f5 100644 --- a/tests/test_rest_app.py +++ b/tests/test_rest_app.py @@ -11,7 +11,7 @@ from mock import MagicMock import AnkiServer from AnkiServer.collection import CollectionManager -from AnkiServer.apps.rest_app import RestApp, CollectionHandlerGroup, DeckHandlerGroup +from AnkiServer.apps.rest_app import RestApp, CollectionHandler, NoteHandler, ModelHandler, DeckHandler, CardHandler from webob.exc import * @@ -127,14 +127,7 @@ class CollectionTestBase(unittest.TestCase): def add_note(self, data): from anki.notes import Note - # TODO: we need to check the input for the correct keys.. Can we automate - # this somehow? Maybe using KeyError or wrapper or something? - - #pprint(self.collection.models.all()) - #pprint(self.collection.models.current()) - model = self.collection.models.byName(data['model']) - #pprint (self.collection.models.fieldNames(model)) note = Note(self.collection, model) for name, value in data['fields'].items(): @@ -143,24 +136,18 @@ class CollectionTestBase(unittest.TestCase): if data.has_key('tags'): note.setTagsFromStr(data['tags']) - ret = self.collection.addNote(note) + self.collection.addNote(note) - def find_notes(self, data): - query = data.get('query', '') - ids = self.collection.getNotes(query) - - -class CollectionHandlerGroupTest(CollectionTestBase): +class CollectionHandlerTest(CollectionTestBase): def setUp(self): - super(CollectionHandlerGroupTest, self).setUp() - self.handler = CollectionHandlerGroup() + super(CollectionHandlerTest, self).setUp() + self.handler = CollectionHandler() def execute(self, name, data): ids = ['collection_name'] func = getattr(self.handler, name) return func(self.collection, data, ids) - def test_list_decks(self): data = {} ret = self.execute('list_decks', data) @@ -174,10 +161,91 @@ class CollectionHandlerGroupTest(CollectionTestBase): ret = self.execute('select_deck', data) self.assertEqual(ret, None); -class DeckHandlerGroupTest(CollectionTestBase): + def test_list_models(self): + data = {} + ret = self.execute('list_models', data) + + # get a sorted name list that we can actually check + names = [model['name'] for model in ret] + names.sort() + + # These are the default models created by Anki in a new collection + default_models = [ + 'Basic', + 'Basic (and reversed card)', + 'Basic (optional reversed card)', + 'Cloze' + ] + + self.assertEqual(names, default_models) + + def test_find_model_by_name(self): + data = {'model': 'Basic'} + ret = self.execute('find_model_by_name', data) + self.assertEqual(ret['name'], 'Basic') + + def test_find_notes(self): + ret = self.execute('find_notes', {}) + self.assertEqual(ret, []) + + # add a note programatically + note = { + 'model': 'Basic', + 'fields': { + 'Front': 'The front', + 'Back': 'The back', + }, + 'tags': "Tag1 Tag2", + } + self.add_note(note) + + # get the id for the one note on this collection + note_id = self.collection.findNotes('')[0] + + ret = self.execute('find_notes', {}) + self.assertEqual(ret, [{'id': note_id}]) + + ret = self.execute('find_notes', {'query': 'tag:Tag1'}) + self.assertEqual(ret, [{'id': note_id}]) + + ret = self.execute('find_notes', {'query': 'tag:TagX'}) + self.assertEqual(ret, []) + + ret = self.execute('find_notes', {'preload': True}) + self.assertEqual(len(ret), 1) + self.assertEqual(ret[0]['id'], note_id) + self.assertEqual(ret[0]['model'], 'Basic') + + def test_add_note(self): + # make sure there are no notes (yet) + self.assertEqual(self.collection.findNotes(''), []) + + # add a note programatically + note = { + 'model': 'Basic', + 'fields': { + 'Front': 'The front', + 'Back': 'The back', + }, + 'tags': "Tag1 Tag2", + } + self.execute('add_note', note) + + notes = self.collection.findNotes('') + self.assertEqual(len(notes), 1) + + note_id = notes[0] + note = self.collection.getNote(note_id) + + self.assertEqual(note.model()['name'], 'Basic') + self.assertEqual(note['Front'], 'The front') + self.assertEqual(note['Back'], 'The back') + self.assertEqual(note.tags, ['Tag1', 'Tag2']) + +class DeckHandlerTest(CollectionTestBase): def setUp(self): - super(DeckHandlerGroupTest, self).setUp() - self.handler = DeckHandlerGroup() + super(DeckHandlerTest, self).setUp() + self.handler = DeckHandler() def execute(self, name, data): ids = ['collection_name', '1']