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()