From 914f56dbc0ec847d06666573e069722ff792fe61 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 8 Aug 2013 13:01:47 +0900 Subject: [PATCH] allow updates to existing notes in .apkg import --- anki/importing/anki2.py | 37 ++++++++++++++++++++++++++----------- tests/support/update1.apkg | Bin 0 -> 3137 bytes tests/support/update2.apkg | Bin 0 -> 3145 bytes tests/test_importing.py | 28 +++++++++++++++++++++++++++- 4 files changed, 53 insertions(+), 12 deletions(-) create mode 100644 tests/support/update1.apkg create mode 100644 tests/support/update2.apkg diff --git a/anki/importing/anki2.py b/anki/importing/anki2.py index f4d3bfd47..758e8fe51 100644 --- a/anki/importing/anki2.py +++ b/anki/importing/anki2.py @@ -9,8 +9,9 @@ from anki.importing.base import Importer from anki.lang import _ from anki.lang import ngettext -MID = 2 GUID = 1 +MID = 2 +MOD = 3 class Anki2Importer(Importer): @@ -63,6 +64,7 @@ class Anki2Importer(Importer): self._changedGuids = {} # iterate over source collection add = [] + update = [] dirty = [] usn = self.dst.usn() dupes = 0 @@ -85,22 +87,35 @@ class Anki2Importer(Importer): # note we have the added the guid self._notes[note[GUID]] = (note[0], note[3], note[MID]) else: + # a duplicate or changed schema - safe to update? dupes += 1 - ## update existing note - not yet tested; for post 2.0 - # newer = note[3] > mod - # if self.allowUpdate and self._mid(mid) == mid and newer: - # localNid = self._notes[guid][0] - # note[0] = localNid - # note[4] = usn - # add.append(note) - # dirty.append(note[0]) + if self.allowUpdate and note[GUID] in self._notes: + oldNid, oldMod, oldMid = self._notes[note[GUID]] + # safe if note types identical + if oldMid == note[MID]: + # will update if incoming note more recent + if oldMod < note[MOD]: + # incoming note should use existing id + note[0] = oldNid + note[4] = usn + note[6] = self._mungeMedia(note[MID], note[6]) + update.append(note) + dirty.append(note[0]) if dupes: - self.log.append(_("Already in collection: %s.") % (ngettext( - "%d note", "%d notes", dupes) % dupes)) + up = len(update) + self.log.append(_("Updated %(a)d of %(b)d existing notes.") % dict( + a=len(update), b=dupes)) + # export info for calling code + self.dupes = dupes + self.added = len(add) + self.updated = len(update) # add to col self.dst.db.executemany( "insert or replace into notes values (?,?,?,?,?,?,?,?,?,?,?)", add) + self.dst.db.executemany( + "insert or replace into notes values (?,?,?,?,?,?,?,?,?,?,?)", + update) self.dst.updateFieldCache(dirty) self.dst.tags.registerNotes(dirty) diff --git a/tests/support/update1.apkg b/tests/support/update1.apkg new file mode 100644 index 0000000000000000000000000000000000000000..5a0643b673d44d7e362f3353792460fe5e9b1eca GIT binary patch literal 3137 zcma(U2Q(boQiv8kT7UBF;+(xrlcFj@dWC5dNWY^04cM1|*L^d; z78cCu{)HPaiF8TC(4nqk#r7;$&+k60l-O5%y4E}cc-?RmAr0*wdI$8?Mj7Mmc#|Yx z5;~ot!W|>y&6yl$@}x@RJwa(=sED4Afx<(omH}TH#0ba7N`pVFo+=P}&qnq`@G z3NlkquoI`F_vmAQWg{aXXhXsr7|o5S;^&IrQh$BZq3&#As4U>qVA-ZwB~2O)#T%_pR}gYh}SZ-Kqc+M zcZYpOzYX3s>^llsXmDxNHSpc5D^eymdJBCnj5YK3bAD>+#*KYoJOlE`IgM@Z1U!-t zgih*{PvnaXVWKw@2Aic^_T%tX$IELn$#o%6q)~|nW4=4?t){o>4d|`;n~{KLa&Zsv zpD0N}Xgf(lFx^8lZk);y!c0AW6XWCi8pS9!<9N{3s`ZYq-zC2m;%{*m9Z&8b&xF`! z^q-%Y0nAI@Z0-yAWU^5fS;nARCua=x!#;U|8=8Ud~%6f*9`Y=P$bUm^L7Lim zuv4nfdz-2lXloR(i`guiTBg*;oHZ`FVcHaLW`waD(|{P2X(m?QBZ_M9fGUe~Sc}oK zc3d>ZfHC{3s>DhdemZrUEl)}%DLd*(9@MWk{%|vKa2M=4FTqwWe(ixt(~P^OC}JHc zG!beq*;_lunKZxxdRyWTm+dC2MOq<1R0BE<`A}xwf#$6fyeyf_%$}b$q zp*0zWQShUghufr`P#gP}H{bo17%8cs!0Lu({gSbtoHk6{CzQ-t#M>9F+|E9o_NNP1qKGyKwue zJ+Wi@rRmouJ2*{=(g%!Kzrucb6OSpK(1;&!^N5WX*-RWym@}LblU*V3&sFVQbmc62 z7&`1wXwVqWlO&*4rBC0s5bN;)CrGQ)j1Q?~yKlZA5E8iC;SSF~RQd@xsdZwwH*v&A2HJU6Ds8(1ouF2pp<>^q1nAkH-T zRzQsSfh8>I{ad**eHSWZnDDnXZigs|ol`<4`wq{~1o{(}L1NNxF@dpXj6~@K%D>r> zaTx^LK*Y=615bV^{l_8YG^e;$0s$Mu`ZtT-_9d0!vH#u>p?!hi&Z;P}sP;6Bgg6_KH{HS>?<3=4H+)yLORWNJhNbr;CBo&CxzAV;3}RLcHW3uUI57?jfn>2{`DP2dWf;>4pL~npxxP&d@O(+e}&N&ix2d?uu>HVXqR1ZhZho26B5{U zAYfL!kgUx!43Ue|H}802c&9-X+QrS}My={w^O$RvajOnA$tZZKn~#wJNC`}%V47`1 zDHm$gA3F1I7I!{NiycyBuFwXfslobEd#aOPecpB{_r;59sJ8u5BCZWp3oJ^lN0G3! zRRvdKi%UTzbq2LHyqs-0<^Oku&~%+KkQ1v3EA>;)2UH#%^VfUD`r#u5|WtQC2J#X4CQZ6ljugJ{G(CL6(uR5hEhu`w?_n^iS0O?+f z>-~x9>b{qsps>nPO-Vv|42t@!xN39sEc50GG(lcmiGU}< z58V))!0+b=iY`{Z0jE+7bp_tIoP6nMk16L}J_oAwg2z{TC<>U~TiiVICIvnBYA2r7 z2>qzJ_u|%qWwJt@S%TX@!p1JSV7hJ*U-=H7(%piq%@s2c%IPA`H%!_7S*5ME03f^h za%?(!!4605!(~eDEyG+>4|{ekW$V_RT5o3w6aId|>u#%nW?NHpg+BbZ@~cN5jzUYI zN`R~|zN~2-vOLd@r>hI4r&6LuXtiuW|z3RHXWKP^XJ@9!MPSf(1FDSWl$U8Z&>-KBkpExq!E-y8* z+lTizy|cAwxRP@yRrxliM(EYt8~s^ks`DpKt2k;p5CBps4$_l8zqhEpDedv2U|%Vn zfIrjwJy4bq$X|H~aZSwn$+~F^EKAG(^U>l&IGsSC)BmLT8jb{bUng MY_|*A@W-I=FA=TjGXMYp literal 0 HcmV?d00001 diff --git a/tests/support/update2.apkg b/tests/support/update2.apkg new file mode 100644 index 0000000000000000000000000000000000000000..5310095ce65c107d8204cf0540a40924cc960078 GIT binary patch literal 3145 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 Mx97F({Pkh@4?}LqlK=n! literal 0 HcmV?d00001 diff --git a/tests/test_importing.py b/tests/test_importing.py index 075ad86d9..940d3642f 100644 --- a/tests/test_importing.py +++ b/tests/test_importing.py @@ -163,7 +163,6 @@ def test_anki1_diffmodels(): assert after == before + 1 assert beforeModels == len(dst.models.all()) - def test_suspended(): # create a new empty deck dst = getEmptyDeck() @@ -205,6 +204,33 @@ def test_anki2_diffmodels(): assert after == before + 1 assert dst.cardCount() == 3 +def test_anki2_updates(): + # create a new empty deck + dst = getEmptyDeck() + tmp = getUpgradeDeckPath("update1.apkg") + imp = AnkiPackageImporter(dst, tmp) + imp.run() + assert imp.dupes == 0 + assert imp.added == 1 + assert imp.updated == 0 + # importing again should be idempotent + imp = AnkiPackageImporter(dst, tmp) + imp.run() + assert imp.dupes == 1 + assert imp.added == 0 + assert imp.updated == 0 + # importing a newer note should update + assert dst.noteCount() == 1 + assert dst.db.scalar("select flds from notes").startswith("hello") + tmp = getUpgradeDeckPath("update2.apkg") + imp = AnkiPackageImporter(dst, tmp) + imp.run() + assert imp.dupes == 1 + assert imp.added == 0 + assert imp.updated == 1 + assert dst.noteCount() == 1 + assert dst.db.scalar("select flds from notes").startswith("goodbye") + def test_csv(): deck = getEmptyDeck() file = unicode(os.path.join(testDir, "support/text-2fields.txt"))