From ffde4a7ff685658591948a65f641ee40f2c46dd7 Mon Sep 17 00:00:00 2001 From: David Snopek Date: Mon, 22 Jul 2013 20:11:53 +0100 Subject: [PATCH] * Added sessions and refactored the handler arguments to only take the collection and (new) request object * Got 'answer_card' actually working * Added some support for the translation built into Anki --- AnkiServer/apps/rest_app.py | 123 ++++++++++++++++++++++++++---------- tests/test_rest_app.py | 70 ++++++++++++++++++-- 2 files changed, 154 insertions(+), 39 deletions(-) diff --git a/AnkiServer/apps/rest_app.py b/AnkiServer/apps/rest_app.py index 4faf092..291f838 100644 --- a/AnkiServer/apps/rest_app.py +++ b/AnkiServer/apps/rest_app.py @@ -14,6 +14,9 @@ except ImportError: import os, logging +import anki.lang +from anki.lang import _ as t + __all__ = ['RestApp', 'RestHandlerBase', 'noReturnValue'] def noReturnValue(func): @@ -33,6 +36,12 @@ class _RestHandlerWrapper(RestHandlerBase): def __call__(self, *args, **kw): return self.func(*args, **kw) +class RestHandlerRequest(object): + def __init__(self, data, ids, session): + self.data = data + self.ids = ids + self.session = session + class RestApp(object): """A WSGI app that implements RESTful operations on Collections, Decks and Cards.""" @@ -67,6 +76,9 @@ class RestApp(object): self.add_handler_group('deck', DeckHandler()) self.add_handler_group('card', CardHandler()) + # hold per collection session data + self.sessions = {} + def add_handler(self, type, name, handler): """Adds a callback handler for a type (collection, deck, card) with a unique name. @@ -221,14 +233,21 @@ class RestApp(object): # parse the request body data = self._parseRequestBody(req) + # get the users session + try: + session = self.sessions[ids[0]] + except KeyError: + session = self.sessions[ids[0]] = {} + # debug from pprint import pprint pprint(data) # run it! col = self.collection_manager.get_collection(collection_path, self.setup_new_collection) + handler_request = RestHandlerRequest(data, ids, session) try: - output = col.execute(handler, [data, ids], {}, hasReturnValue) + output = col.execute(handler, [handler_request], {}, hasReturnValue) except Exception, e: logging.error(e) return HTTPInternalServerError() @@ -245,24 +264,24 @@ class CollectionHandler(RestHandlerBase): # MODELS - Store fields definitions and templates for notes # - def list_models(self, col, data, ids): + def list_models(self, col, req): # 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): + def find_model_by_name(self, col, req): # This is already a list of dicts, so it doesn't need to be serialized - return col.models.byName(data['model']) + return col.models.byName(req.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', '') + def find_notes(self, col, req): + query = req.data.get('query', '') ids = col.findNotes(query) - if data.get('preload', False): + if req.data.get('preload', False): nodes = [NoteHandler._serialize(col.getNote(id)) for id in ids] else: nodes = [{'id': id} for id in ids] @@ -270,22 +289,22 @@ class CollectionHandler(RestHandlerBase): return nodes @noReturnValue - def add_note(self, col, data, ids): + def add_note(self, col, req): from anki.notes import Note # TODO: I think this would be better with 'model' for the name # and 'mid' for the model id. - if type(data['model']) in (str, unicode): - model = col.models.byName(data['model']) + if type(req.data['model']) in (str, unicode): + model = col.models.byName(req.data['model']) else: - model = col.models.get(data['model']) + model = col.models.get(req.data['model']) note = Note(col, model) - for name, value in data['fields'].items(): + for name, value in req.data['fields'].items(): note[name] = value - if data.has_key('tags'): - note.setTagsFromStr(data['tags']) + if req.data.has_key('tags'): + note.setTagsFromStr(req.data['tags']) col.addNote(note) @@ -293,27 +312,27 @@ class CollectionHandler(RestHandlerBase): # DECKS - Groups of cards # - def list_decks(self, col, data, ids): + def list_decks(self, col, req): # 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']) + def select_deck(self, col, req): + col.decks.select(req.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', '') + def find_cards(self, col, req): + query = req.data.get('query', '') ids = col.findCards(query) - if data.get('preload', False): - cards = [CardHandler._serialize(col.getCard(id)) for id in ids] + if req.data.get('preload', False): + cards = [CardHandler._serialize(col.getCard(id)) for id in req.ids] else: - cards = [{'id': id} for id in ids] + cards = [{'id': id} for id in req.ids] return cards @@ -322,9 +341,29 @@ class CollectionHandler(RestHandlerBase): # @noReturnValue - def reset_scheduler(self, col, data, ids): + def reset_scheduler(self, col, req): col.sched.reset() + 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) + + # + # GLOBAL / MISC + # + + @noReturnValue + def set_language(self, col, req): + anki.lang.setLang(req.data['code']) + class ImportExportHandler(RestHandlerBase): """Handler group for the 'collection' type, but it's not added by default.""" @@ -354,15 +393,15 @@ class ImportExportHandler(RestHandlerBase): return importer_class - def import_file(self, col, data, ids): + def import_file(self, col, req): import AnkiServer.importer import tempfile # get the importer class - importer_class = self._get_importer_class(data) + importer_class = self._get_importer_class(req.data) # get the file data - filedata = self._get_filedata(data) + filedata = self._get_filedata(req.data) # write the file data to a temporary file try: @@ -379,8 +418,8 @@ class ImportExportHandler(RestHandlerBase): class ModelHandler(RestHandlerBase): """Default handler group for 'model' type.""" - def field_names(self, col, data, ids): - model = col.models.get(ids[1]) + def field_names(self, col, req): + model = col.models.get(req.ids[1]) if model is None: raise HTTPNotFound() return col.models.fieldNames(model) @@ -398,8 +437,8 @@ class NoteHandler(RestHandlerBase): # TODO: do more stuff! return d - def index(self, col, data, ids): - note = col.getNote(ids[1]) + def index(self, col, req): + note = col.getNote(req.ids[1]) return self._serialize(note) class DeckHandler(RestHandlerBase): @@ -431,16 +470,31 @@ class DeckHandler(RestHandlerBase): l.reverse() # Loop through and add the ease and estimated time (in seconds) - return [(ease, label, col.sched.nextIvl(card, ease)) for ease, label in enumerate(l, 1)] + 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, data, ids): - deck = self._get_deck(col, ids) + def next_card(self, col, req): + deck = self._get_deck(col, req.ids) + + # TODO: maybe this should use col.renderQA()? 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() + result = CardHandler._serialize(card) result['answer_buttons'] = self._get_answer_buttons(col, card) @@ -471,11 +525,14 @@ class CardHandler(RestHandlerBase): 'reps': card.reps, 'type': card.type, 'usn': card.usn, + 'timerStarted': card.timerStarted, } return d # Our entry point def make_app(global_conf, **local_conf): + # TODO: we should setup the default language from conf! + # setup the logger from AnkiServer.utils import setup_logging setup_logging(local_conf.get('logging.config_file')) diff --git a/tests/test_rest_app.py b/tests/test_rest_app.py index 2189390..299851a 100644 --- a/tests/test_rest_app.py +++ b/tests/test_rest_app.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import os import shutil @@ -11,7 +12,7 @@ from mock import MagicMock import AnkiServer from AnkiServer.collection import CollectionManager -from AnkiServer.apps.rest_app import RestApp, CollectionHandler, ImportExportHandler, NoteHandler, ModelHandler, DeckHandler, CardHandler +from AnkiServer.apps.rest_app import RestApp, RestHandlerRequest, CollectionHandler, ImportExportHandler, NoteHandler, ModelHandler, DeckHandler, CardHandler from webob.exc import * @@ -158,7 +159,8 @@ class CollectionHandlerTest(CollectionTestBase): def execute(self, name, data): ids = ['collection_name'] func = getattr(self.handler, name) - return func(self.collection, data, ids) + req = RestHandlerRequest(data, ids, {}) + return func(self.collection, req) def test_list_decks(self): data = {} @@ -246,6 +248,38 @@ class CollectionHandlerTest(CollectionTestBase): self.assertEqual(note['Back'], 'The back') self.assertEqual(note.tags, ['Tag1', 'Tag2']) + def test_set_language(self): + import anki.lang + + self.assertEqual(anki.lang._('Again'), 'Again') + + try: + data = {'code': 'pl'} + self.execute('set_language', data) + self.assertEqual(anki.lang._('Again'), u'Znowu') + finally: + # return everything to normal! + anki.lang.setLang('en') + + def test_answer_card(self): + import time + + self.add_default_note() + + # instantiate a deck handler to get the card + deck_handler = DeckHandler() + deck_request = RestHandlerRequest({}, ['c', '1'], {}) + card = deck_handler.next_card(self.collection, deck_request) + self.assertEqual(card['reps'], 0) + + + self.execute('answer_card', {'id': card['id'], 'ease': 2, 'timerStarted': time.time()}) + + # reset the scheduler and try to get the next card again - there should be none! + self.collection.sched.reset() + card = deck_handler.next_card(self.collection, deck_request) + self.assertEqual(card['reps'], 1) + class ImportExportHandlerTest(CollectionTestBase): export_rows = [ ['Card front 1', 'Card back 1', 'Tag1 Tag2'], @@ -259,7 +293,8 @@ class ImportExportHandlerTest(CollectionTestBase): def execute(self, name, data): ids = ['collection_name'] func = getattr(self.handler, name) - return func(self.collection, data, ids) + req = RestHandlerRequest(data, ids, {}) + return func(self.collection, req) def generate_text_export(self): # Create a simple export file @@ -310,7 +345,8 @@ class DeckHandlerTest(CollectionTestBase): def execute(self, name, data): ids = ['collection_name', '1'] func = getattr(self.handler, name) - return func(self.collection, data, ids) + req = RestHandlerRequest(data, ids, {}) + return func(self.collection, req) def test_next_card(self): ret = self.execute('next_card', {}) @@ -324,12 +360,34 @@ class DeckHandlerTest(CollectionTestBase): card_id = self.collection.findCards('')[0] self.collection.sched.reset() - ret = self.execute('next_card', {}) + + # get the card in Polish so we can test translation too + anki.lang.setLang('pl') + try: + ret = self.execute('next_card', {}) + finally: + anki.lang.setLang('en') + self.assertEqual(ret['id'], card_id) self.assertEqual(ret['nid'], note_id) self.assertEqual(ret['question'], 'The front') self.assertEqual(ret['answer'], 'The front\n\n
\n\nThe back') - self.assertEqual(ret['answer_buttons'], [(1, 'Again', 60), (2, 'Good', 600), (3, 'Easy', 345600)]) + self.assertEqual(ret['answer_buttons'], [ + {'ease': 1, + 'label': 'Again', + 'string_label': u'Znowu', + 'interval': 60, + 'string_interval': '<1 minuta'}, + {'ease': 2, + 'label': 'Good', + 'string_label': u'Dobra', + 'interval': 600, + 'string_interval': '<10 minut'}, + {'ease': 3, + 'label': 'Easy', + 'string_label': u'Łatwa', + 'interval': 345600, + 'string_interval': '4 dni'}]) def test_next_card_five_times(self): self.add_default_note(5)