diff --git a/actions/cards.md b/actions/cards.md index a62b987..bd5a983 100644 --- a/actions/cards.md +++ b/actions/cards.md @@ -271,7 +271,14 @@ "css":"p {font-family:Arial;}", "cardId": 1498938915662, "interval": 16, - "note":1502298033753 + "note":1502298033753, + "ord": 1, + "type": 0, + "queue": 0, + "due": 1, + "reps": 1, + "lapses": 0, + "left": 6 }, { "answer": "back content", @@ -286,7 +293,14 @@ "css":"p {font-family:Arial;}", "cardId": 1502098034048, "interval": 23, - "note":1502298033753 + "note":1502298033753, + "ord": 1, + "type": 0, + "queue": 0, + "due": 1, + "reps": 1, + "lapses": 0, + "left": 6 } ], "error": null diff --git a/actions/decks.md b/actions/decks.md index fea9845..b402877 100644 --- a/actions/decks.md +++ b/actions/decks.md @@ -329,3 +329,73 @@ "error": null } ``` + +* **updateCompleteDeck** + + Pastes all transmitted data into the database and reloads the collection. + You can send a deckName and corresponding cards, notes & models. + All cards are assumed to belong to the given deck. + All notes referenced by given cards should be present. + All models referenced by given notes should be present. + + *Sample request*: + ```json + { + "action": "updateCompleteDeck", + "version": 6, + "params": { + "data": { + "deck": "test3", + "cards": { + "1485369472028": { + "id": 1485369472028, + "nid": 1485369340204, + "ord": 0, + "type": 0, + "queue": 0, + "due": 1186031, + "factor": 0, + "ivl": 0, + "reps": 0, + "lapses": 0, + "left": 0 + } + }, + "notes": { + "1485369340204": { + "id": 1485369340204, + "mid": 1375786181313, + "fields": [ + "frontValue", + "backValue" + ], + "tags": [ + "aTag" + ] + } + }, + "models": { + "1375786181313": { + "id": 1375786181313, + "name": "anotherModel", + "fields": [ + "Front", + "Back" + ], + "templateNames": [ + "Card 1" + ] + } + } + } + } + } + ``` + + *Sample result*: + ```json + { + "result": null, + "error": null + } + ``` \ No newline at end of file diff --git a/actions/miscellaneous.md b/actions/miscellaneous.md index 3347067..523990a 100644 --- a/actions/miscellaneous.md +++ b/actions/miscellaneous.md @@ -168,3 +168,23 @@ "error": null } ``` + +* **reloadCollection** + + Tells anki to reload all data from the database. + + *Sample request*: + ```json + { + "action": "reloadCollection", + "version": 6 + } + ``` + + *Sample result*: + ```json + { + "result": null, + "error": null + } + ``` \ No newline at end of file diff --git a/actions/statistics.md b/actions/statistics.md index b116d0c..0ff29b0 100644 --- a/actions/statistics.md +++ b/actions/statistics.md @@ -42,3 +42,81 @@ "result": "
lots of HTML here
" } ``` + +* **cardReviews** + + Requests all card reviews for a specified deck after a certain time. + `startID` is the latest unix time not included in the result. + Returns a list of 9-tuples `(reviewTime, cardID, usn, buttonPressed, newInterval, previousInterval, newFactor, reviewDuration, reviewType)` + + *Sample request*: + ```json + { + "action": "cardReviews", + "version": 6, + "params": { + "deck": "default", + "startID": 1594194095740 + } + } + ``` + + *Sample result*: + ```json + { + "result": [ + [1594194095746, 1485369733217, -1, 3, 4, -60, 2500, 6157, 0], + [1594201393292, 1485369902086, -1, 1, -60, -60, 0, 4846, 0] + ], + "error": null + } + ``` + +* **getLatestReviewID** + + Returns the unix time of the latest review for the given deck. 0 if no review has ever been made for the deck. + + *Sample request*: + ```json + { + "action": "getLatestReviewID", + "version": 6, + "params": { + "deck": "default" + } + } + ``` + + *Sample result*: + ```json + { + "result": 1594194095746, + "error": null + } + ``` + +* **insertReviews** + + Inserts the given reviews into the database. Required format: list of 9-tuples `(reviewTime, cardID, usn, buttonPressed, newInterval, previousInterval, newFactor, reviewDuration, reviewType)` + + *Sample request*: + ```json + { + "action": "insertReviews", + "version": 6, + "params": { + "reviews": [ + [1594194095746, 1485369733217, -1, 3, 4, -60, 2500, 6157, 0], + [1594201393292, 1485369902086, -1, 1, -60, -60, 0, 4846, 0] + ] + } + } + ``` + + *Sample result*: + ```json + { + "result": null, + "error": null + } + ``` diff --git a/plugin/__init__.py b/plugin/__init__.py index 6235464..bb50c41 100644 --- a/plugin/__init__.py +++ b/plugin/__init__.py @@ -34,6 +34,7 @@ import anki.storage import aqt from anki.exporting import AnkiPackageExporter from anki.importing import AnkiPackageImporter +from anki.utils import joinFields, intTime, guid64, fieldChecksum from . import web, util @@ -877,13 +878,20 @@ class AnkiConnect: 'question': util.getQuestion(card), 'answer': util.getAnswer(card), 'modelName': model['name'], + 'ord': card.ord, 'deckName': self.deckNameFromId(card.did), 'css': model['css'], 'factor': card.factor, #This factor is 10 times the ease percentage, # so an ease of 310% would be reported as 3100 'interval': card.ivl, - 'note': card.nid + 'note': card.nid, + 'type': card.type, + 'queue': card.queue, + 'due': card.due, + 'reps': card.reps, + 'lapses': card.lapses, + 'left': card.left, }) except TypeError as e: # Anki will give a TypeError if the card ID does not exist. @@ -895,6 +903,68 @@ class AnkiConnect: return result + @util.api() + def cardReviews(self, deck, startID): + return self.database().all("select id, cid, usn, ease, ivl, lastIvl, factor, time, type from revlog " + "where id>? and cid in (select id from cards where did=?)", + startID, self.decks().id(deck)) + + @util.api() + def reloadCollection(self): + self.collection().reset() + + @util.api() + def getLatestReviewID(self, deck): + return self.database().scalar("select max(id) from revlog where cid in (select id from cards where did=?)", + self.decks().id(deck)) or 0 + + @util.api() + def updateCompleteDeck(self, data): + self.startEditing() + did = self.decks().id(data["deck"]) + self.decks().flush() + model_manager = self.collection().models + for _, card in data["cards"].items(): + self.database().execute( + "replace into cards (id, nid, did, ord, type, queue, due, ivl, factor, reps, lapses, left, " + "mod, usn, odue, odid, flags, data) " + "values (" + "?," * (12 + 6 - 1) + "?)", + card["id"], card["nid"], did, card["ord"], card["type"], card["queue"], card["due"], + card["ivl"], card["factor"], card["reps"], card["lapses"], card["left"], + intTime(), -1, 0, 0, 0, 0 + ) + note = data["notes"][str(card["nid"])] + tags = self.collection().tags.join(self.collection().tags.canonify(note["tags"])) + self.database().execute( + "replace into notes(id, mid, tags, flds," + "guid, mod, usn, flags, data, sfld, csum) values (" + "?," * (4 + 7 - 1) + "?)", + note["id"], note["mid"], tags, joinFields(note["fields"]), + guid64(), intTime(), -1, 0, 0, "", fieldChecksum(note["fields"][0]) + ) + model = data["models"][str(note["mid"])] + if not model_manager.get(model["id"]): + model_o = model_manager.new(model["name"]) + for field_name in model["fields"]: + field = model_manager.newField(field_name) + model_manager.addField(model_o, field) + for template_name in model["templateNames"]: + template = model_manager.newTemplate(template_name) + model_manager.addTemplate(model_o, template) + model_o["id"] = model["id"] + model_manager.update(model_o) + model_manager.flush() + + self.stopEditing() + + @util.api() + def insertReviews(self, reviews): + if len(reviews) == 0: return + sql = "insert into revlog(id,cid,usn,ease,ivl,lastIvl,factor,time,type) values " + for row in reviews: + sql += "(%s)," % ",".join(map(str, row)) + sql = sql[:-1] + self.database().execute(sql) + @util.api() def notesInfo(self, notes): result = [] diff --git a/tests/test_decks.py b/tests/test_decks.py index 7edd95a..2997d64 100755 --- a/tests/test_decks.py +++ b/tests/test_decks.py @@ -67,5 +67,32 @@ class TestDecks(unittest.TestCase): deckConfigId = util.invoke('cloneDeckConfigId', cloneFrom=deckConfigId, name='test') self.assertFalse(deckConfigId) + # updateCompleteDeck + util.invoke("updateCompleteDeck", data={ + "deck": "test3", + "cards": { + "12": { + "id": 12, "nid": 23, "ord": 0, "type": 0, "queue": 0, + "due": 1186031, "factor": 0, "ivl": 0, "reps": 0, "lapses": 0, "left": 0 + } + }, + "notes": { + "23": { + "id": 23, "mid": 34, "fields": ["frontValue", "backValue"], "tags": ["aTag"] + } + }, + "models": { + "34": { + "id": 34, "fields": ["Front", "Back"], "templateNames": ["Card 1"], "name": "anotherModel", + } + } + }) + deckNames = util.invoke("deckNames") + self.assertIn("test3", deckNames) + cardIDs = util.invoke('findCards', query='deck:test3') + self.assertEqual(len(cardIDs), 1) + self.assertEqual(cardIDs[0], 12) + + if __name__ == '__main__': unittest.main() diff --git a/tests/test_misc.py b/tests/test_misc.py index e0762bc..b67a2e3 100755 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -52,6 +52,9 @@ class TestMisc(unittest.TestCase): deckNames = util.invoke('deckNames') self.assertIn(deckName, deckNames) + # reloadCollection + util.invoke("reloadCollection") + if __name__ == '__main__': unittest.main() diff --git a/tests/test_stats.py b/tests/test_stats.py index 0f33487..3c90764 100644 --- a/tests/test_stats.py +++ b/tests/test_stats.py @@ -6,7 +6,15 @@ import unittest import util -class TestMisc(unittest.TestCase): +class TestStats(unittest.TestCase): + def setUp(self): + util.invoke('createDeck', deck='test') + note = {'deckName': 'test', 'modelName': 'Basic', 'fields': {'Front': 'front1', 'Back': 'back1'}, 'tags': ['tag1']} + self.noteId = util.invoke('addNote', note=note) + + def tearDown(self): + util.invoke('deleteDecks', decks=['test'], cardsToo=True) + def runTest(self): # getNumCardsReviewedToday result = util.invoke('getNumCardsReviewedToday') @@ -16,6 +24,20 @@ class TestMisc(unittest.TestCase): result = util.invoke('getCollectionStatsHTML') self.assertIsInstance(result, str) + # no reviews for new deck + self.assertEqual(len(util.invoke("cardReviews", deck="test", startID=0)), 0) + self.assertEqual(util.invoke("getLatestReviewID", deck="test"), 0) + + # add reviews + cardId = int(util.invoke('findCards', query='deck:test')[0]) + latestID = 123456 # small enough to not interfere with existing reviews + util.invoke("insertReviews", reviews=[ + [latestID-1, cardId, -1, 3, 4, -60, 2500, 6157, 0], + [latestID, cardId, -1, 1, -60, -60, 0, 4846, 0] + ]) + self.assertEqual(len(util.invoke("cardReviews", deck="test", startID=0)), 2) + self.assertEqual(util.invoke("getLatestReviewID", deck="test"), latestID) + if __name__ == '__main__': unittest.main()