From 5f9ebed2f729cd32b68a443ef42de89f741afe04 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Sat, 15 Jun 2019 16:47:16 +0200 Subject: [PATCH 01/68] int to constants --- anki/cards.py | 8 +- anki/collection.py | 12 +- anki/consts.py | 28 +++++ anki/find.py | 18 +-- anki/sched.py | 265 +++++++++++++++++++++--------------------- anki/schedv2.py | 281 ++++++++++++++++++++++----------------------- aqt/browser.py | 27 ++--- 7 files changed, 334 insertions(+), 305 deletions(-) diff --git a/anki/cards.py b/anki/cards.py index b8c78e17651..3342fa9dd4f 100644 --- a/anki/cards.py +++ b/anki/cards.py @@ -34,8 +34,8 @@ def __init__(self, col, id=None): self.id = timestampID(col.db, "cards") self.did = 1 self.crt = intTime() - self.type = 0 - self.queue = 0 + self.type = CARD_NEW + self.queue = QUEUE_NEW_CRAM self.ivl = 0 self.factor = 0 self.reps = 0 @@ -73,7 +73,7 @@ def flush(self): self.mod = intTime() self.usn = self.col.usn() # bug check - if self.queue == 2 and self.odue and not self.col.decks.isDyn(self.did): + if self.queue == QUEUE_REV and self.odue and not self.col.decks.isDyn(self.did): runHook("odueInvalid") assert self.due < 4294967296 self.col.db.execute( @@ -104,7 +104,7 @@ def flushSched(self): self.mod = intTime() self.usn = self.col.usn() # bug checks - if self.queue == 2 and self.odue and not self.col.decks.isDyn(self.did): + if self.queue == QUEUE_REV and self.odue and not self.col.decks.isDyn(self.did): runHook("odueInvalid") assert self.due < 4294967296 self.col.db.execute( diff --git a/anki/collection.py b/anki/collection.py index acf15164752..fff48b4e57c 100644 --- a/anki/collection.py +++ b/anki/collection.py @@ -823,8 +823,8 @@ def fixIntegrity(self): "Deleted %d cards with missing note.", cnt) % cnt) self.remCards(ids) # cards with odue set when it shouldn't be - ids = self.db.list(""" -select id from cards where odue > 0 and (type=1 or queue=2) and not odid""") + ids = self.db.list(f""" +select id from cards where odue > 0 and (type={CARD_LRN} or queue={CARD_DUE}) and not odid""") if ids: cnt = len(ids) problems.append( @@ -835,7 +835,7 @@ def fixIntegrity(self): # cards with odid set when not in a dyn deck dids = [id for id in self.decks.allIds() if not self.decks.isDyn(id)] ids = self.db.list(""" -select id from cards where odid > 0 and did in %s""" % ids2str(dids)) + select id from cards where odid > 0 and did in %s""" % ids2str(dids)) if ids: cnt = len(ids) problems.append( @@ -850,14 +850,14 @@ def fixIntegrity(self): self.updateFieldCache(self.models.nids(m)) # new cards can't have a due position > 32 bits, so wrap items over # 2 million back to 1 million - curs.execute(""" + curs.execute(f""" update cards set due=1000000+due%1000000,mod=?,usn=? where due>=1000000 -and type=0""", [intTime(), self.usn()]) +and type = {CARD_NEW}""", [intTime(), self.usn()]) if curs.rowcount: problems.append("Found %d new cards with a due number >= 1,000,000 - consider repositioning them in the Browse screen." % curs.rowcount) # new card position self.conf['nextPos'] = self.db.scalar( - "select max(due)+1 from cards where type = 0") or 0 + f"select max(due)+1 from cards where type = {CARD_NEW}") or 0 # reviews should have a reasonable due # ids = self.db.list( "select id from cards where queue = 2 and due > 100000") diff --git a/anki/consts.py b/anki/consts.py index dde15fe937e..24389208bd4 100644 --- a/anki/consts.py +++ b/anki/consts.py @@ -54,6 +54,34 @@ HELP_SITE="http://ankisrs.net/docs/manual.html" +# Queue types +QUEUE_SCHED_BURIED = -3 +QUEUE_USER_BURIED = -2 +QUEUE_SUSPENDED = -1 +QUEUE_NEW_CRAM = 0 +QUEUE_LRN = 1 +QUEUE_REV = 2 +QUEUE_DAY_LRN = 3 +QUEUE_PREVIEW = 4 + +# Revlog types +REVLOG_LRN = 0 +REVLOG_REV = 1 +REVLOG_RELRN = 2 +REVLOG_CRAM = 3 + +# Card types +CARD_NEW = 0 +CARD_LRN = 1 +CARD_DUE = 2 +CARD_FILTERED = 3 + +# Buttons +BUTTON_ONE = 1 +BUTTON_TWO = 2 +BUTTON_THREE = 3 +BUTTON_FOUR = 4 + # Labels ########################################################################## diff --git a/anki/find.py b/anki/find.py index 9c95e6ceae5..c0080fc7d19 100644 --- a/anki/find.py +++ b/anki/find.py @@ -257,20 +257,20 @@ def _findCardState(self, args): (val, args) = args if val in ("review", "new", "learn"): if val == "review": - n = 2 + n = CARD_DUE elif val == "new": - n = 0 + n = CARD_NEW else: - return "queue in (1, 3)" + return f"queue in ({QUEUE_LRN}, {QUEUE_DAY_LRN})" return "type = %d" % n elif val == "suspended": - return "c.queue = -1" + return f"c.queue = {QUEUE_SUSPENDED}" elif val == "buried": - return "c.queue in (-2, -3)" + return f"c.queue in ({QUEUE_USER_BURIED}, {QUEUE_SCHED_BURIED})" elif val == "due": - return """ -(c.queue in (2,3) and c.due <= %d) or -(c.queue = 1 and c.due <= %d)""" % ( + return f""" +(c.queue in ({QUEUE_REV},{QUEUE_DAY_LRN}) and c.due <= %d) or +(c.queue = {QUEUE_LRN} and c.due <= %d)""" % ( self.col.sched.today, self.col.sched.dayCutoff) def _findFlag(self, args): @@ -333,7 +333,7 @@ def _findProp(self, args): if prop == "due": val += self.col.sched.today # only valid for review/daily learning - q.append("(c.queue in (2,3))") + q.append(f"(c.queue in ({QUEUE_REV},{QUEUE_DAY_LRN}))") elif prop == "ease": prop = "factor" val = int(val*1000) diff --git a/anki/sched.py b/anki/sched.py index dcce7963fa6..4bf10580428 100644 --- a/anki/sched.py +++ b/anki/sched.py @@ -62,28 +62,28 @@ def answerCard(self, card, ease): self._burySiblings(card) card.reps += 1 # former is for logging new cards, latter also covers filt. decks - card.wasNew = card.type == 0 - wasNewQ = card.queue == 0 + card.wasNew = card.type == CARD_NEW + wasNewQ = card.queue == QUEUE_NEW_CRAM if wasNewQ: # came from the new queue, move to learning - card.queue = 1 + card.queue = QUEUE_LRN # if it was a new card, it's now a learning card - if card.type == 0: - card.type = 1 + if card.type == CARD_NEW: + card.type = CARD_LRN # init reps to graduation card.left = self._startingLeft(card) # dynamic? - if card.odid and card.type == 2: + if card.odid and card.type == CARD_DUE: if self._resched(card): # reviews get their ivl boosted on first sight card.ivl = self._dynIvlBoost(card) card.odue = self.today + card.ivl self._updateStats(card, 'new') - if card.queue in (1, 3): + if card.queue in (QUEUE_LRN, QUEUE_DAY_LRN): self._answerLrnCard(card, ease) if not wasNewQ: self._updateStats(card, 'lrn') - elif card.queue == 2: + elif card.queue == QUEUE_REV: self._answerRevCard(card, ease) self._updateStats(card, 'rev') else: @@ -97,7 +97,7 @@ def counts(self, card=None): counts = [self.newCount, self.lrnCount, self.revCount] if card: idx = self.countIdx(card) - if idx == 1: + if idx == QUEUE_LRN: counts[1] += card.left // 1000 else: counts[idx] += 1 @@ -105,12 +105,12 @@ def counts(self, card=None): def dueForecast(self, days=7): "Return counts over next DAYS. Includes today." - daysd = dict(self.col.db.all(""" + daysd = dict(self.col.db.all(f""" select due, count() from cards -where did in %s and queue = 2 +where did in %s and queue = {QUEUE_REV} and due between ? and ? group by due -order by due""" % self._deckLimit(), +order by due""" % (self._deckLimit()), self.today, self.today+days-1)) for d in range(days): @@ -122,40 +122,40 @@ def dueForecast(self, days=7): return ret def countIdx(self, card): - if card.queue == 3: - return 1 + if card.queue == QUEUE_DAY_LRN: + return QUEUE_LRN return card.queue def answerButtons(self, card): if card.odue: # normal review in dyn deck? - if card.odid and card.queue == 2: - return 4 + if card.odid and card.queue == QUEUE_REV: + return BUTTON_FOUR conf = self._lrnConf(card) - if card.type in (0,1) or len(conf['delays']) > 1: - return 3 - return 2 - elif card.queue == 2: - return 4 + if card.type in (CARD_NEW,CARD_LRN) or len(conf['delays']) > 1: + return BUTTON_THREE + return BUTTON_TWO + elif card.queue == QUEUE_REV: + return BUTTON_FOUR else: - return 3 + return BUTTON_THREE def unburyCards(self): "Unbury cards." self.col.conf['lastUnburied'] = self.today self.col.log( - self.col.db.list("select id from cards where queue = -2")) + self.col.db.list(f"select id from cards where queue = {QUEUE_USER_BURIED}")) self.col.db.execute( - "update cards set queue=type where queue = -2") + f"update cards set queue=type where queue = {QUEUE_USER_BURIED}") def unburyCardsForDeck(self): sids = ids2str(self.col.decks.active()) self.col.log( - self.col.db.list("select id from cards where queue = -2 and did in %s" - % sids)) + self.col.db.list(f"select id from cards where queue = {QUEUE_USER_BURIED} and did in %s" + % (sids))) self.col.db.execute( - "update cards set mod=?,usn=?,queue=type where queue = -2 and did in %s" - % sids, intTime(), self.col.usn()) + f"update cards set mod=?,usn=?,queue=type where queue = {QUEUE_USER_BURIED} and did in %s" + % (sids), intTime(), self.col.usn()) # Rev/lrn/time daily stats ########################################################################## @@ -330,9 +330,9 @@ def _getCard(self): ########################################################################## def _resetNewCount(self): - cntFn = lambda did, lim: self.col.db.scalar(""" + cntFn = lambda did, lim: self.col.db.scalar(f""" select count() from (select 1 from cards where -did = ? and queue = 0 limit ?)""", did, lim) +did = ? and queue = {QUEUE_NEW_CRAM} limit ?)""", did, lim) self.newCount = self._walkingCount(self._deckNewLimitSingle, cntFn) def _resetNew(self): @@ -351,8 +351,8 @@ def _fillNew(self): lim = min(self.queueLimit, self._deckNewLimit(did)) if lim: # fill the queue with the current did - self._newQueue = self.col.db.list(""" - select id from cards where did = ? and queue = 0 order by due,ord limit ?""", did, lim) + self._newQueue = self.col.db.list(f""" + select id from cards where did = ? and queue = {QUEUE_NEW_CRAM} order by due,ord limit ?""" , did, lim) if self._newQueue: self._newQueue.reverse() return True @@ -411,9 +411,9 @@ def _newForDeck(self, did, lim): if not lim: return 0 lim = min(lim, self.reportLimit) - return self.col.db.scalar(""" + return self.col.db.scalar(f""" select count() from -(select 1 from cards where did = ? and queue = 0 limit ?)""", did, lim) +(select 1 from cards where did = ? and queue = {QUEUE_NEW_CRAM} limit ?)""", did, lim) def _deckNewLimitSingle(self, g): "Limit for deck without parent limits." @@ -424,25 +424,25 @@ def _deckNewLimitSingle(self, g): def totalNewForCurrentDeck(self): return self.col.db.scalar( - """ + f""" select count() from cards where id in ( -select id from cards where did in %s and queue = 0 limit ?)""" - % ids2str(self.col.decks.active()), self.reportLimit) +select id from cards where did in %s and queue = {QUEUE_NEW_CRAM} limit ?)""" + % (ids2str(self.col.decks.active()), self.reportLimit)) # Learning queues ########################################################################## def _resetLrnCount(self): # sub-day - self.lrnCount = self.col.db.scalar(""" + self.lrnCount = self.col.db.scalar(f""" select sum(left/1000) from (select left from cards where -did in %s and queue = 1 and due < ? limit %d)""" % ( +did in %s and queue = {QUEUE_LRN} and due < ? limit %d)""" % ( self._deckLimit(), self.reportLimit), self.dayCutoff) or 0 # day - self.lrnCount += self.col.db.scalar(""" -select count() from cards where did in %s and queue = 3 -and due <= ? limit %d""" % (self._deckLimit(), self.reportLimit), + self.lrnCount += self.col.db.scalar(f""" +select count() from cards where did in %s and queue = {QUEUE_DAY_LRN} +and due <= ? limit %d""" % (self._deckLimit(), self.reportLimit), self.today) def _resetLrn(self): @@ -457,9 +457,9 @@ def _fillLrn(self): return False if self._lrnQueue: return True - self._lrnQueue = self.col.db.all(""" + self._lrnQueue = self.col.db.all(f""" select due, id from cards where -did in %s and queue = 1 and due < :lim +did in %s and queue = {QUEUE_LRN} and due < :lim limit %d""" % (self._deckLimit(), self.reportLimit), lim=self.dayCutoff) # as it arrives sorted by did first, we need to sort it self._lrnQueue.sort() @@ -485,9 +485,9 @@ def _fillLrnDay(self): while self._lrnDids: did = self._lrnDids[0] # fill the queue with the current did - self._lrnDayQueue = self.col.db.list(""" + self._lrnDayQueue = self.col.db.list(f""" select id from cards where -did = ? and queue = 3 and due <= ? limit ?""", +did = ? and queue = {QUEUE_DAY_LRN} and due <= ? limit ?""", did, self.today, self.queueLimit) if self._lrnDayQueue: # order @@ -510,25 +510,25 @@ def _answerLrnCard(self, card, ease): # ease 1=no, 2=yes, 3=remove conf = self._lrnConf(card) if card.odid and not card.wasNew: - type = 3 - elif card.type == 2: - type = 2 + type = REVLOG_CRAM + elif card.type == CARD_DUE: + type = REVLOG_RELRN else: - type = 0 + type = REVLOG_LRN leaving = False # lrnCount was decremented once when card was fetched lastLeft = card.left # immediate graduate? - if ease == 3: + if ease == BUTTON_THREE: self._rescheduleAsRev(card, conf, True) leaving = True # graduation time? - elif ease == 2 and (card.left%1000)-1 <= 0: + elif ease == BUTTON_TWO and (card.left%1000)-1 <= 0: self._rescheduleAsRev(card, conf, False) leaving = True else: # one step towards graduation - if ease == 2: + if ease == BUTTON_TWO: # decrement real left count and recalculate left today left = (card.left % 1000) - 1 card.left = self._leftToday(conf['delays'], left)*1000 + left @@ -555,7 +555,7 @@ def _answerLrnCard(self, card, ease): # if the queue is not empty and there's nothing else to do, make # sure we don't put it at the head of the queue and end up showing # it twice in a row - card.queue = 1 + card.queue = QUEUE_LRN if self._lrnQueue and not self.revCount and not self.newCount: smallestDue = self._lrnQueue[0][0] card.due = max(card.due, smallestDue+1) @@ -565,7 +565,7 @@ def _answerLrnCard(self, card, ease): # day learn queue ahead = ((card.due - self.dayCutoff) // 86400) + 1 card.due = self.today + ahead - card.queue = 3 + card.queue = QUEUE_DAY_LRN self._logLrn(card, ease, conf, leaving, type, lastLeft) def _delayForGrade(self, conf, left): @@ -581,13 +581,13 @@ def _delayForGrade(self, conf, left): return delay*60 def _lrnConf(self, card): - if card.type == 2: + if card.type == CARD_DUE: return self._lapseConf(card) else: return self._newConf(card) def _rescheduleAsRev(self, card, conf, early): - lapse = card.type == 2 + lapse = card.type == CARD_DUE if lapse: if self._resched(card): card.due = max(self.today+1, card.odue) @@ -596,8 +596,8 @@ def _rescheduleAsRev(self, card, conf, early): card.odue = 0 else: self._rescheduleNew(card, conf, early) - card.queue = 2 - card.type = 2 + card.queue = QUEUE_REV + card.type = CARD_DUE # if we were dynamic, graduating means moving back to the old deck resched = self._resched(card) if card.odid: @@ -606,11 +606,12 @@ def _rescheduleAsRev(self, card, conf, early): card.odid = 0 # if rescheduling is off, it needs to be set back to a new card if not resched and not lapse: - card.queue = card.type = 0 + card.queue = QUEUE_NEW_CRAM + card.type = CARD_NEW card.due = self.col.nextID("pos") def _startingLeft(self, card): - if card.type == 2: + if card.type == CARD_DUE: conf = self._lapseConf(card) else: conf = self._lrnConf(card) @@ -632,7 +633,7 @@ def _leftToday(self, delays, left, now=None): return ok+1 def _graduatingIvl(self, card, conf, early, adj=True): - if card.type == 2: + if card.type == CARD_DUE: # lapsed card being relearnt if card.odid: if conf['resched']: @@ -679,27 +680,27 @@ def removeLrn(self, ids=None): # with the index than scan the table extra = " and did in "+ids2str(self.col.decks.allIds()) # review cards in relearning - self.col.db.execute(""" + self.col.db.execute(f""" update cards set -due = odue, queue = 2, mod = %d, usn = %d, odue = 0 -where queue in (1,3) and type = 2 +due = odue, queue = {QUEUE_REV}, mod = %d, usn = %d, odue = 0 +where queue in ({QUEUE_LRN},{QUEUE_DAY_LRN}) and type = {CARD_DUE} %s """ % (intTime(), self.col.usn(), extra)) # new cards in learning self.forgetCards(self.col.db.list( - "select id from cards where queue in (1,3) %s" % extra)) + f"select id from cards where queue in ({QUEUE_LRN}, {QUEUE_DAY_LRN}) %s" % extra)) def _lrnForDeck(self, did): cnt = self.col.db.scalar( - """ + f""" select sum(left/1000) from -(select left from cards where did = ? and queue = 1 and due < ? limit ?)""", +(select left from cards where did = ? and queue = {QUEUE_LRN} and due < ? limit ?)""", did, intTime() + self.col.conf['collapseTime'], self.reportLimit) or 0 return cnt + self.col.db.scalar( - """ + f""" select count() from -(select 1 from cards where did = ? and queue = 3 -and due <= ? limit ?)""", +(select 1 from cards where did = ? and queue = {QUEUE_DAY_LRN} +and due <= ? limit ?)""" , did, self.today, self.reportLimit) # Reviews @@ -717,17 +718,17 @@ def _deckRevLimitSingle(self, d): def _revForDeck(self, did, lim): lim = min(lim, self.reportLimit) return self.col.db.scalar( - """ + f""" select count() from -(select 1 from cards where did = ? and queue = 2 +(select 1 from cards where did = ? and queue = {QUEUE_REV} and due <= ? limit ?)""", did, self.today, lim) def _resetRevCount(self): def cntFn(did, lim): - return self.col.db.scalar(""" + return self.col.db.scalar(f""" select count() from (select id from cards where -did = ? and queue = 2 and due <= ? limit %d)""" % lim, +did = ? and queue = {QUEUE_REV} and due <= ? limit %d)""" % (lim), did, self.today) self.revCount = self._walkingCount( self._deckRevLimitSingle, cntFn) @@ -747,9 +748,9 @@ def _fillRev(self): lim = min(self.queueLimit, self._deckRevLimit(did)) if lim: # fill the queue with the current did - self._revQueue = self.col.db.list(""" + self._revQueue = self.col.db.list(f""" select id from cards where -did = ? and queue = 2 and due <= ? limit ?""", +did = ? and queue = {QUEUE_REV} and due <= ? limit ?""", did, self.today, lim) if self._revQueue: # ordering @@ -783,15 +784,15 @@ def totalRevForCurrentDeck(self): return self.col.db.scalar( """ select count() from cards where id in ( -select id from cards where did in %s and queue = 2 and due <= ? limit ?)""" - % ids2str(self.col.decks.active()), self.today, self.reportLimit) +select id from cards where did in %s and queue = {QUEUE_REV} and due <= ? limit ?)""" + % (ids2str(self.col.decks.active()), self.today, self.reportLimit)) # Answering a review card ########################################################################## def _answerRevCard(self, card, ease): delay = 0 - if ease == 1: + if ease == BUTTON_ONE: delay = self._rescheduleLapse(card) else: self._rescheduleRev(card, ease) @@ -810,7 +811,7 @@ def _rescheduleLapse(self, card): card.odue = card.due # if suspended as a leech, nothing to do delay = 0 - if self._checkLeech(card, conf) and card.queue == -1: + if self._checkLeech(card, conf) and card.queue == QUEUE_SUSPENDED: return delay # if no relearning steps, nothing to do if not conf['delays']: @@ -821,16 +822,16 @@ def _rescheduleLapse(self, card): delay = self._delayForGrade(conf, 0) card.due = int(delay + time.time()) card.left = self._startingLeft(card) - # queue 1 + # queue LRN if card.due < self.dayCutoff: self.lrnCount += card.left // 1000 - card.queue = 1 + card.queue = QUEUE_LRN heappush(self._lrnQueue, (card.due, card.id)) else: # day learn queue ahead = ((card.due - self.dayCutoff) // 86400) + 1 card.due = self.today + ahead - card.queue = 3 + card.queue = QUEUE_DAY_LRN return delay def _nextLapseIvl(self, card, conf): @@ -877,11 +878,11 @@ def _nextRevIvl(self, card, ease): ivl3 = self._constrainedIvl((card.ivl + delay // 2) * fct, conf, ivl2) ivl4 = self._constrainedIvl( (card.ivl + delay) * fct * conf['ease4'], conf, ivl3) - if ease == 2: + if ease == BUTTON_TWO: interval = ivl2 - elif ease == 3: + elif ease == BUTTON_THREE: interval = ivl3 - elif ease == 4: + elif ease == BUTTON_FOUR: interval = ivl4 # interval capped? return min(interval, conf['maxIvl']) @@ -963,10 +964,10 @@ def emptyDyn(self, did, lim=None): lim = "did = %s" % did self.col.log(self.col.db.list("select id from cards where %s" % lim)) # move out of cram queue - self.col.db.execute(""" -update cards set did = odid, queue = (case when type = 1 then 0 -else type end), type = (case when type = 1 then 0 else type end), -due = odue, odue = 0, odid = 0, usn = ? where %s""" % lim, + self.col.db.execute(f""" +update cards set did = odid, queue = (case when type = {CARD_LRN} then {QUEUE_NEW_CRAM} +else type end), type = (case when type = {CARD_LRN} then {CARD_NEW} else type end), +due = odue, odue = 0, odid = 0, usn = ? where %s""" % (lim), self.col.usn()) def remFromDyn(self, cids): @@ -990,7 +991,7 @@ def _dynOrder(self, o, l): elif o == DYN_DUE: t = "c.due" elif o == DYN_DUEPRIORITY: - t = "(case when queue=2 and due <= %d then (ivl / cast(%d-due+0.001 as real)) else 100000+due end)" % ( + t = "(case when queue=%d and due <= %d then (ivl / cast(%d-due+0.001 as real)) else 100000+due end)" % (QUEUE_REV, self.today, self.today) else: # if we don't understand the term, default to due order @@ -1006,9 +1007,9 @@ def _moveToDyn(self, did, ids): data.append((did, -100000+c, u, id)) # due reviews stay in the review queue. careful: can't use # "odid or did", as sqlite converts to boolean - queue = """ -(case when type=2 and (case when odue then odue <= %d else due <= %d end) - then 2 else 0 end)""" + queue = f""" +(case when type={CARD_DUE} and (case when odue then odue <= %d else due <= %d end) + then {QUEUE_REV} else {QUEUE_NEW_CRAM} end)""" queue %= (self.today, self.today) self.col.db.executemany(""" update cards set @@ -1017,7 +1018,7 @@ def _moveToDyn(self, did, ids): did = ?, queue = %s, due = ?, usn = ? where id = ?""" % queue, data) def _dynIvlBoost(self, card): - assert card.odid and card.type == 2 + assert card.odid and card.type == CARD_DUE assert card.factor elapsed = card.ivl - (card.odue - self.today) factor = ((card.factor/1000)+1.2)/2 @@ -1049,7 +1050,7 @@ def _checkLeech(self, card, conf): if card.odid: card.did = card.odid card.odue = card.odid = 0 - card.queue = -1 + card.queue = QUEUE_SUSPENDED # notify UI runHook("leech", card) return True @@ -1184,20 +1185,20 @@ def _nextDueMsg(self): def revDue(self): "True if there are any rev cards due." return self.col.db.scalar( - ("select 1 from cards where did in %s and queue = 2 " - "and due <= ? limit 1") % self._deckLimit(), + (f"select 1 from cards where did in %s and queue = {QUEUE_REV} " + "and due <= ? limit 1") % (self._deckLimit()), self.today) def newDue(self): "True if there are any new cards due." return self.col.db.scalar( - ("select 1 from cards where did in %s and queue = 0 " - "limit 1") % self._deckLimit()) + (f"select 1 from cards where did in %s and queue = {QUEUE_NEW_CRAM} " + "limit 1") % (self._deckLimit(),)) def haveBuried(self): sdids = ids2str(self.col.decks.active()) cnt = self.col.db.scalar( - "select 1 from cards where queue = -2 and did in %s limit 1" % sdids) + f"select 1 from cards where queue = {QUEUE_USER_BURIED} and did in %s limit 1" % (sdids)) return not not cnt # Next time reports @@ -1215,9 +1216,9 @@ def nextIvlStr(self, card, ease, short=False): def nextIvl(self, card, ease): "Return the next interval for CARD, in seconds." - if card.queue in (0,1,3): + if card.queue in (QUEUE_NEW_CRAM, QUEUE_LRN, QUEUE_DAY_LRN): return self._nextLrnIvl(card, ease) - elif ease == 1: + elif ease == BUTTON_ONE: # lapsed conf = self._lapseConf(card) if conf['delays']: @@ -1229,13 +1230,13 @@ def nextIvl(self, card, ease): # this isn't easily extracted from the learn code def _nextLrnIvl(self, card, ease): - if card.queue == 0: + if card.queue == QUEUE_NEW_CRAM: card.left = self._startingLeft(card) conf = self._lrnConf(card) - if ease == 1: + if ease == BUTTON_ONE: # fail return self._delayForGrade(conf, len(conf['delays'])) - elif ease == 3: + elif ease == BUTTON_THREE: # early removal if not self._resched(card): return 0 @@ -1259,23 +1260,23 @@ def suspendCards(self, ids): self.remFromDyn(ids) self.removeLrn(ids) self.col.db.execute( - "update cards set queue=-1,mod=?,usn=? where id in "+ + (f"update cards set queue={QUEUE_SUSPENDED},mod=?,usn=? where id in ")+ ids2str(ids), intTime(), self.col.usn()) def unsuspendCards(self, ids): "Unsuspend cards." self.col.log(ids) self.col.db.execute( - "update cards set queue=type,mod=?,usn=? " - "where queue = -1 and id in "+ ids2str(ids), + (f"update cards set queue=type,mod=?,usn=? " + f"where queue = {QUEUE_SUSPENDED} and id in ")+ ids2str(ids), intTime(), self.col.usn()) def buryCards(self, cids): self.col.log(cids) self.remFromDyn(cids) self.removeLrn(cids) - self.col.db.execute(""" -update cards set queue=-2,mod=?,usn=? where id in """+ids2str(cids), + self.col.db.execute((f""" + update cards set queue={QUEUE_USER_BURIED},mod=?,usn=? where id in """)+ids2str(cids), intTime(), self.col.usn()) def buryNote(self, nid): @@ -1294,11 +1295,11 @@ def _burySiblings(self, card): rconf = self._revConf(card) buryRev = rconf.get("bury", True) # loop through and remove from queues - for cid,queue in self.col.db.execute(""" + for cid,queue in self.col.db.execute(f""" select id, queue from cards where nid=? and id!=? -and (queue=0 or (queue=2 and due<=?))""", +and (queue={QUEUE_NEW_CRAM} or (queue={QUEUE_REV} and due<=?))""", card.nid, card.id, self.today): - if queue == 2: + if queue == QUEUE_REV: if buryRev: toBury.append(cid) # if bury disabled, we still discard to give same-day spacing @@ -1306,7 +1307,7 @@ def _burySiblings(self, card): self._revQueue.remove(cid) except ValueError: pass - else: + else:#Queue new Cram # if bury disabled, we still discard to give same-day spacing if buryNew: toBury.append(cid) @@ -1317,7 +1318,7 @@ def _burySiblings(self, card): # then bury if toBury: self.col.db.execute( - "update cards set queue=-2,mod=?,usn=? where id in "+ids2str(toBury), + (f"update cards set queue={QUEUE_USER_BURIED},mod=?,usn=? where id in ")+ids2str(toBury), intTime(), self.col.usn()) self.col.log(toBury) @@ -1328,10 +1329,10 @@ def forgetCards(self, ids): "Put cards at the end of the new queue." self.remFromDyn(ids) self.col.db.execute( - "update cards set type=0,queue=0,ivl=0,due=0,odue=0,factor=?" - " where id in "+ids2str(ids), STARTING_FACTOR) + (f"update cards set type={CARD_NEW},queue={QUEUE_NEW_CRAM},ivl=0,due=0,odue=0,factor=?" + " where id in ")+ids2str(ids), STARTING_FACTOR) pmax = self.col.db.scalar( - "select max(due) from cards where type=0") or 0 + f"select max(due) from cards where type={CARD_NEW}") or 0 # takes care of mod + usn self.sortCards(ids, start=pmax+1) self.col.log(ids) @@ -1346,8 +1347,8 @@ def reschedCards(self, ids, imin, imax): d.append(dict(id=id, due=r+t, ivl=max(1, r), mod=mod, usn=self.col.usn(), fact=STARTING_FACTOR)) self.remFromDyn(ids) - self.col.db.executemany(""" -update cards set type=2,queue=2,ivl=:ivl,due=:due,odue=0, + self.col.db.executemany(f""" +update cards set type={CARD_DUE},queue={QUEUE_REV},ivl=:ivl,due=:due,odue=0, usn=:usn,mod=:mod,factor=:fact where id=:id""", d) self.col.log(ids) @@ -1357,12 +1358,12 @@ def resetCards(self, ids): sids = ids2str(ids) # we want to avoid resetting due number of existing new cards on export nonNew = self.col.db.list( - "select id from cards where id in %s and (queue != 0 or type != 0)" - % sids) + f"select id from cards where id in %s and (queue != {QUEUE_NEW_CRAM} or type != {CARD_NEW})" + % (sids)) # reset all cards self.col.db.execute( - "update cards set reps=0,lapses=0,odid=0,odue=0,queue=0" - " where id in %s" % sids + f"update cards set reps=0,lapses=0,odid=0,odue=0,queue={QUEUE_NEW_CRAM}" + " where id in %s" % (sids) ) # and forget any non-new cards, changing their due numbers self.forgetCards(nonNew) @@ -1395,18 +1396,18 @@ def sortCards(self, cids, start=1, step=1, shuffle=False, shift=False): # shift? if shift: low = self.col.db.scalar( - "select min(due) from cards where due >= ? and type = 0 " - "and id not in %s" % scids, + f"select min(due) from cards where due >= ? and type = {CARD_NEW} " + "and id not in %s" % (scids), start) if low is not None: shiftby = high - low + 1 - self.col.db.execute(""" + self.col.db.execute(f""" update cards set mod=?, usn=?, due=due+? where id not in %s -and due >= ? and queue = 0""" % scids, now, self.col.usn(), shiftby, low) +and due >= ? and queue = {QUEUE_NEW_CRAM}""" % (scids), now, self.col.usn(), shiftby, low) # reorder cards d = [] for id, nid in self.col.db.execute( - "select id, nid from cards where type = 0 and id in "+scids): + (f"select id, nid from cards where type = {CARD_NEW} and id in ")+scids): d.append(dict(now=now, due=due[nid], usn=self.col.usn(), cid=id)) self.col.db.executemany( "update cards set due=:due,mod=:now,usn=:usn where id = :cid", d) @@ -1421,7 +1422,7 @@ def orderCards(self, did): def resortConf(self, conf): for did in self.col.decks.didsForConf(conf): - if conf['new']['order'] == 0: + if conf['new']['order'] == NEW_CARDS_RANDOM: self.randomizeCards(did) else: self.orderCards(did) diff --git a/anki/schedv2.py b/anki/schedv2.py index 04e002699af..b0357cfaa5c 100644 --- a/anki/schedv2.py +++ b/anki/schedv2.py @@ -80,18 +80,18 @@ def _answerCard(self, card, ease): card.reps += 1 - if card.queue == 0: + if card.queue == QUEUE_NEW_CRAM: # came from the new queue, move to learning - card.queue = 1 - card.type = 1 + card.queue = QUEUE_LRN + card.type = CARD_LRN # init reps to graduation card.left = self._startingLeft(card) # update daily limit self._updateStats(card, 'new') - if card.queue in (1, 3): + if card.queue in (QUEUE_LRN, QUEUE_DAY_LRN): self._answerLrnCard(card, ease) - elif card.queue == 2: + elif card.queue == QUEUE_REV: self._answerRevCard(card, ease) # update daily limit self._updateStats(card, 'rev') @@ -106,12 +106,12 @@ def _answerCard(self, card, ease): def _answerCardPreview(self, card, ease): assert 1 <= ease <= 2 - if ease == 1: + if ease == BUTTON_ONE: # repeat after delay - card.queue = 4 + card.queue = QUEUE_PREVIEW card.due = intTime() + self._previewDelay(card) self.lrnCount += 1 - else: + else: #BUTTON_TWO # restore original card state and remove from filtered deck self._restorePreviewCard(card) self._removeFromFiltered(card) @@ -125,12 +125,12 @@ def counts(self, card=None): def dueForecast(self, days=7): "Return counts over next DAYS. Includes today." - daysd = dict(self.col.db.all(""" + daysd = dict(self.col.db.all(f""" select due, count() from cards -where did in %s and queue = 2 +where did in %s and queue = {QUEUE_REV} and due between ? and ? group by due -order by due""" % self._deckLimit(), +order by due""" % (self._deckLimit()), self.today, self.today+days-1)) for d in range(days): @@ -142,8 +142,8 @@ def dueForecast(self, days=7): return ret def countIdx(self, card): - if card.queue in (3,4): - return 1 + if card.queue in (QUEUE_DAY_LRN,QUEUE_PREVIEW): + return QUEUE_LRN return card.queue def answerButtons(self, card): @@ -337,9 +337,9 @@ def _getCard(self): ########################################################################## def _resetNewCount(self): - cntFn = lambda did, lim: self.col.db.scalar(""" + cntFn = lambda did, lim: self.col.db.scalar(f""" select count() from (select 1 from cards where -did = ? and queue = 0 limit ?)""", did, lim) +did = ? and queue = {QUEUE_NEW_CRAM} limit ?)""", did, lim) self.newCount = self._walkingCount(self._deckNewLimitSingle, cntFn) def _resetNew(self): @@ -358,8 +358,8 @@ def _fillNew(self): lim = min(self.queueLimit, self._deckNewLimit(did)) if lim: # fill the queue with the current did - self._newQueue = self.col.db.list(""" - select id from cards where did = ? and queue = 0 order by due,ord limit ?""", did, lim) + self._newQueue = self.col.db.list(f""" + select id from cards where did = ? and queue = {QUEUE_NEW_CRAM} order by due,ord limit ?""", did, lim) if self._newQueue: self._newQueue.reverse() return True @@ -418,9 +418,9 @@ def _newForDeck(self, did, lim): if not lim: return 0 lim = min(lim, self.reportLimit) - return self.col.db.scalar(""" + return self.col.db.scalar(f""" select count() from -(select 1 from cards where did = ? and queue = 0 limit ?)""", did, lim) +(select 1 from cards where did = ? and queue = {QUEUE_NEW_CRAM} limit ?)""", did, lim) def _deckNewLimitSingle(self, g): "Limit for deck without parent limits." @@ -431,9 +431,9 @@ def _deckNewLimitSingle(self, g): def totalNewForCurrentDeck(self): return self.col.db.scalar( - """ + f""" select count() from cards where id in ( -select id from cards where did in %s and queue = 0 limit ?)""" +select id from cards where did in %s and queue = {QUEUE_NEW_CRAM} limit ?)""" % ids2str(self.col.decks.active()), self.reportLimit) # Learning queues @@ -453,20 +453,20 @@ def _maybeResetLrn(self, force): def _resetLrnCount(self): # sub-day - self.lrnCount = self.col.db.scalar(""" -select count() from cards where did in %s and queue = 1 -and due < ?""" % ( - self._deckLimit()), + self.lrnCount = self.col.db.scalar(f""" +select count() from cards where did in %s and queue = {QUEUE_LRN} +and due < ?""" % + self._deckLimit(), self._lrnCutoff) or 0 # day - self.lrnCount += self.col.db.scalar(""" -select count() from cards where did in %s and queue = 3 -and due <= ?""" % (self._deckLimit()), + self.lrnCount += self.col.db.scalar(f""" +select count() from cards where did in %s and queue = {QUEUE_DAY_LRN} +and due <= ?""" % self._deckLimit(), self.today) # previews - self.lrnCount += self.col.db.scalar(""" -select count() from cards where did in %s and queue = 4 -""" % (self._deckLimit())) + self.lrnCount += self.col.db.scalar(f""" +select count() from cards where did in %s and queue = {QUEUE_PREVIEW} +""" % self._deckLimit()) def _resetLrn(self): self._updateLrnCutoff(force=True) @@ -482,9 +482,9 @@ def _fillLrn(self): if self._lrnQueue: return True cutoff = intTime() + self.col.conf['collapseTime'] - self._lrnQueue = self.col.db.all(""" + self._lrnQueue = self.col.db.all(f""" select due, id from cards where -did in %s and queue in (1,4) and due < :lim +did in %s and queue in ({QUEUE_LRN},{QUEUE_PREVIEW}) and due < :lim limit %d""" % (self._deckLimit(), self.reportLimit), lim=cutoff) # as it arrives sorted by did first, we need to sort it self._lrnQueue.sort() @@ -511,9 +511,9 @@ def _fillLrnDay(self): while self._lrnDids: did = self._lrnDids[0] # fill the queue with the current did - self._lrnDayQueue = self.col.db.list(""" + self._lrnDayQueue = self.col.db.list((f""" select id from cards where -did = ? and queue = 3 and due <= ? limit ?""", +did = ? and queue = {QUEUE_DAY_LRN} and due <= ? limit ?"""), did, self.today, self.queueLimit) if self._lrnDayQueue: # order @@ -534,28 +534,28 @@ def _getLrnDayCard(self): def _answerLrnCard(self, card, ease): conf = self._lrnConf(card) - if card.type in (2,3): - type = 2 + if card.type in (CARD_DUE,CARD_FILTERED): + type = REVLOG_RELRN else: - type = 0 + type = REVLOG_LRN # lrnCount was decremented once when card was fetched lastLeft = card.left leaving = False # immediate graduate? - if ease == 4: + if ease == BUTTON_FOUR: self._rescheduleAsRev(card, conf, True) leaving = True # next step? - elif ease == 3: + elif ease == BUTTON_THREE: # graduation time? if (card.left%1000)-1 <= 0: self._rescheduleAsRev(card, conf, False) leaving = True else: self._moveToNextStep(card, conf) - elif ease == 2: + elif ease == BUTTON_TWO: self._repeatStep(card, conf) else: # back to first step @@ -571,7 +571,7 @@ def _moveToFirstStep(self, card, conf): card.left = self._startingLeft(card) # relearning card? - if card.type == 3: + if card.type == CARD_FILTERED: self._updateRevIvlOnFail(card, conf) return self._rescheduleLrnCard(card, conf) @@ -599,7 +599,7 @@ def _rescheduleLrnCard(self, card, conf, delay=None): maxExtra = min(300, int(delay*0.25)) fuzz = random.randrange(0, maxExtra) card.due = min(self.dayCutoff-1, card.due + fuzz) - card.queue = 1 + card.queue = QUEUE_LRN if card.due < (intTime() + self.col.conf['collapseTime']): self.lrnCount += 1 # if the queue is not empty and there's nothing else to do, make @@ -614,7 +614,7 @@ def _rescheduleLrnCard(self, card, conf, delay=None): # day learn queue ahead = ((card.due - self.dayCutoff) // 86400) + 1 card.due = self.today + ahead - card.queue = 3 + card.queue = QUEUE_DAY_LRN return delay def _delayForGrade(self, conf, left): @@ -640,13 +640,13 @@ def _delayForRepeatingGrade(self, conf, left): return avg def _lrnConf(self, card): - if card.type in (2, 3): + if card.type in (CARD_DUE, CARD_FILTERED): return self._lapseConf(card) else: return self._newConf(card) def _rescheduleAsRev(self, card, conf, early): - lapse = card.type in (2,3) + lapse = card.type in (CARD_DUE, CARD_FILTERED) if lapse: self._rescheduleGraduatingLapse(card) @@ -659,11 +659,11 @@ def _rescheduleAsRev(self, card, conf, early): def _rescheduleGraduatingLapse(self, card): card.due = self.today+card.ivl - card.queue = 2 - card.type = 2 + card.queue = QUEUE_REV + card.type = CARD_DUE def _startingLeft(self, card): - if card.type == 3: + if card.type == CARD_FILTERED: conf = self._lapseConf(card) else: conf = self._lrnConf(card) @@ -685,7 +685,7 @@ def _leftToday(self, delays, left, now=None): return ok+1 def _graduatingIvl(self, card, conf, early, fuzz=True): - if card.type in (2,3): + if card.type in (CARD_DUE, CARD_FILTERED): return card.ivl if not early: # graduate @@ -702,7 +702,8 @@ def _rescheduleNew(self, card, conf, early): card.ivl = self._graduatingIvl(card, conf, early) card.due = self.today+card.ivl card.factor = conf['initialFactor'] - card.type = card.queue = 2 + card.type = CARD_DUE + card.queue = QUEUE_REV def _logLrn(self, card, ease, conf, leaving, type, lastLeft): lastIvl = -(self._delayForGrade(conf, lastLeft)) @@ -721,14 +722,14 @@ def log(): def _lrnForDeck(self, did): cnt = self.col.db.scalar( - """ + f""" select count() from -(select null from cards where did = ? and queue = 1 and due < ? limit ?)""", +(select null from cards where did = ? and queue = {QUEUE_LRN} and due < ? limit ?)""", did, intTime() + self.col.conf['collapseTime'], self.reportLimit) or 0 return cnt + self.col.db.scalar( - """ + f""" select count() from -(select null from cards where did = ? and queue = 3 +(select null from cards where did = ? and queue = {QUEUE_DAY_LRN} and due <= ? limit ?)""", did, self.today, self.reportLimit) @@ -764,18 +765,19 @@ def _revForDeck(self, did, lim, childMap): dids = [did] + self.col.decks.childDids(did, childMap) lim = min(lim, self.reportLimit) return self.col.db.scalar( - """ + f""" select count() from -(select 1 from cards where did in %s and queue = 2 +(select 1 from cards where did in %s and queue = {QUEUE_REV} and due <= ? limit ?)""" % ids2str(dids), self.today, lim) def _resetRevCount(self): lim = self._currentRevLimit() - self.revCount = self.col.db.scalar(""" + self.revCount = self.col.db.scalar(f""" select count() from (select id from cards where -did in %s and queue = 2 and due <= ? limit %d)""" % ( - ids2str(self.col.decks.active()), lim), self.today) +did in %s and queue = {QUEUE_REV} and due <= ? limit {lim})""" % + ids2str(self.col.decks.active()), + self.today) def _resetRev(self): self._resetRevCount() @@ -789,11 +791,11 @@ def _fillRev(self): lim = min(self.queueLimit, self._currentRevLimit()) if lim: - self._revQueue = self.col.db.list(""" + self._revQueue = self.col.db.list(f""" select id from cards where -did in %s and queue = 2 and due <= ? +did in %s and queue = {QUEUE_REV} and due <= ? order by due -limit ?""" % (ids2str(self.col.decks.active())), +limit ?""" % ids2str(self.col.decks.active()), self.today, lim) if self._revQueue: @@ -821,9 +823,9 @@ def _getRevCard(self): def totalRevForCurrentDeck(self): return self.col.db.scalar( - """ + f""" select count() from cards where id in ( -select id from cards where did in %s and queue = 2 and due <= ? limit ?)""" +select id from cards where did in %s and queue = {QUEUE_REV} and due <= ? limit ?)""" % ids2str(self.col.decks.active()), self.today, self.reportLimit) # Answering a review card @@ -832,9 +834,9 @@ def totalRevForCurrentDeck(self): def _answerRevCard(self, card, ease): delay = 0 early = card.odid and (card.odue > self.today) - type = early and 3 or 1 + type = early and REVLOG_CRAM or REVLOG_REV - if ease == 1: + if ease == BUTTON_ONE: delay = self._rescheduleLapse(card) else: self._rescheduleRev(card, ease, early) @@ -847,10 +849,10 @@ def _rescheduleLapse(self, card): card.lapses += 1 card.factor = max(1300, card.factor-200) - suspended = self._checkLeech(card, conf) and card.queue == -1 + suspended = self._checkLeech(card, conf) and card.queue == QUEUE_SUSPENDED if conf['delays'] and not suspended: - card.type = 3 + card.type = CARD_FILTERED delay = self._moveToFirstStep(card, conf) else: # no relearning steps @@ -858,7 +860,7 @@ def _rescheduleLapse(self, card): self._rescheduleAsRev(card, conf, early=False) # need to reset the queue after rescheduling if suspended: - card.queue = -1 + card.queue = QUEUE_SUSPENDED delay = 0 return delay @@ -910,11 +912,11 @@ def _nextRevIvl(self, card, ease, fuzz): else: hardMin = 0 ivl2 = self._constrainedIvl(card.ivl * hardFactor, conf, hardMin, fuzz) - if ease == 2: + if ease == BUTTON_TWO: return ivl2 ivl3 = self._constrainedIvl((card.ivl + delay // 2) * fct, conf, ivl2, fuzz) - if ease == 3: + if ease == BUTTON_THREE: return ivl3 ivl4 = self._constrainedIvl( @@ -961,7 +963,7 @@ def _updateEarlyRevIvl(self, card, ease): # next interval for card when answered early+correctly def _earlyReviewIvl(self, card, ease): - assert card.odid and card.type == 2 + assert card.odid and card.type == CARD_DUE assert card.factor assert ease > 1 @@ -973,14 +975,14 @@ def _earlyReviewIvl(self, card, ease): # early 3/4 reviews shouldn't decrease previous interval minNewIvl = 1 - if ease == 2: + if ease == BUTTON_TWO: factor = conf.get("hardFactor", 1.2) # hard cards shouldn't have their interval decreased by more than 50% # of the normal factor minNewIvl = factor / 2 - elif ease == 3: + elif ease == BUTTON_THREE: factor = card.factor / 1000 - else: # ease == 4: + else: # ease == BUTTON_FOUR: factor = card.factor / 1000 ease4 = conf['ease4'] # 1.3 -> 1.15 @@ -1062,8 +1064,7 @@ def _dynOrder(self, o, l): elif o == DYN_DUE: t = "c.due" elif o == DYN_DUEPRIORITY: - t = "(case when queue=2 and due <= %d then (ivl / cast(%d-due+0.001 as real)) else 100000+due end)" % ( - self.today, self.today) + t = f"(case when queue={QUEUE_REV} and due <= %d then (ivl / cast(%d-due+0.001 as real)) else 100000+due end)" % (self.today, self.today) else: # if we don't understand the term, default to due order t = "c.due" @@ -1080,8 +1081,7 @@ def _moveToDyn(self, did, ids, start=-100000): queue = "" if not deck['resched']: - queue = ",queue=2" - + queue = f",queue={QUEUE_REV}" query = """ update cards set odid = did, odue = due, @@ -1104,11 +1104,11 @@ def _restorePreviewCard(self, card): # learning and relearning cards may be seconds-based or day-based; # other types map directly to queues - if card.type in (1, 3): + if card.type in (CARD_LRN, CARD_FILTERED): if card.odue > 1000000000: - card.queue = 1 + card.queue = QUEUE_LRN else: - card.queue = 3 + card.queue = QUEUE_DAY_LRN else: card.queue = card.type @@ -1130,7 +1130,7 @@ def _checkLeech(self, card, conf): # handle a = conf['leechAction'] if a == 0: - card.queue = -1 + card.queue = QUEUE_SUSPENDED # notify UI runHook("leech", card) return True @@ -1283,26 +1283,26 @@ def _nextDueMsg(self): def revDue(self): "True if there are any rev cards due." return self.col.db.scalar( - ("select 1 from cards where did in %s and queue = 2 " + (f"select 1 from cards where did in %s and queue = {QUEUE_REV} " "and due <= ? limit 1") % self._deckLimit(), self.today) def newDue(self): "True if there are any new cards due." return self.col.db.scalar( - ("select 1 from cards where did in %s and queue = 0 " + (f"select 1 from cards where did in %s and queue = {QUEUE_NEW_CRAM} " "limit 1") % self._deckLimit()) def haveBuriedSiblings(self): sdids = ids2str(self.col.decks.active()) cnt = self.col.db.scalar( - "select 1 from cards where queue = -2 and did in %s limit 1" % sdids) + f"select 1 from cards where queue = {QUEUE_USER_BURIED} and did in %s limit 1" % sdids) return not not cnt def haveManuallyBuried(self): sdids = ids2str(self.col.decks.active()) cnt = self.col.db.scalar( - "select 1 from cards where queue = -3 and did in %s limit 1" % sdids) + f"select 1 from cards where queue = {QUEUE_SCHED_BURIED} and did in %s limit 1" % sdids) return not not cnt def haveBuried(self): @@ -1325,14 +1325,14 @@ def nextIvl(self, card, ease): "Return the next interval for CARD, in seconds." # preview mode? if self._previewingCard(card): - if ease == 1: + if ease == BUTTON_ONE: return self._previewDelay(card) return 0 # (re)learning? - if card.queue in (0,1,3): + if card.queue in (QUEUE_NEW_CRAM, QUEUE_LRN, QUEUE_DAY_LRN): return self._nextLrnIvl(card, ease) - elif ease == 1: + elif ease == BUTTON_ONE: # lapse conf = self._lapseConf(card) if conf['delays']: @@ -1348,17 +1348,17 @@ def nextIvl(self, card, ease): # this isn't easily extracted from the learn code def _nextLrnIvl(self, card, ease): - if card.queue == 0: + if card.queue == QUEUE_NEW_CRAM: card.left = self._startingLeft(card) conf = self._lrnConf(card) - if ease == 1: + if ease == BUTTON_ONE: # fail return self._delayForGrade(conf, len(conf['delays'])) - elif ease == 2: + elif ease == BUTTON_TWO: return self._delayForRepeatingGrade(conf, card.left) - elif ease == 4: + elif ease == BUTTON_FOUR: return self._graduatingIvl(card, conf, True, fuzz=False) * 86400 - else: # ease == 3 + else: # ease == BUTTON_THREE left = card.left%1000 - 1 if left <= 0: # graduate @@ -1371,19 +1371,18 @@ def _nextLrnIvl(self, card, ease): # learning and relearning cards may be seconds-based or day-based; # other types map directly to queues - _restoreQueueSnippet = """ -queue = (case when type in (1,3) then - (case when (case when odue then odue else due end) > 1000000000 then 1 else 3 end) + _restoreQueueSnippet = f""" +queue = (case when type in ({CARD_LRN},{CARD_FILTERED}) then + (case when (case when odue then odue else due end) > 1000000000 then {QUEUE_LRN} else {QUEUE_DAY_LRN} end) else type -end) +end) """ - def suspendCards(self, ids): "Suspend cards." self.col.log(ids) self.col.db.execute( - "update cards set queue=-1,mod=?,usn=? where id in "+ + ("update cards set queue=%d,mod=?,usn=? where id in "%QUEUE_SUSPENDED)+ ids2str(ids), intTime(), self.col.usn()) def unsuspendCards(self, ids): @@ -1391,11 +1390,11 @@ def unsuspendCards(self, ids): self.col.log(ids) self.col.db.execute( ("update cards set %s,mod=?,usn=? " - "where queue = -1 and id in %s") % (self._restoreQueueSnippet, ids2str(ids)), + f"where queue = {QUEUE_SUSPENDED} and id in %s") % (self._restoreQueueSnippet, ids2str(ids)), intTime(), self.col.usn()) def buryCards(self, cids, manual=True): - queue = manual and -3 or -2 + queue = manual and QUEUE_SCHED_BURIED or QUEUE_USER_BURIED self.col.log(cids) self.col.db.execute(""" update cards set queue=?,mod=?,usn=? where id in """+ids2str(cids), @@ -1410,17 +1409,17 @@ def buryNote(self, nid): def unburyCards(self): "Unbury all buried cards in all decks." self.col.log( - self.col.db.list("select id from cards where queue in (-2, -3)")) + self.col.db.list(f"select id from cards where queue in ({QUEUE_USER_BURIED}, {QUEUE_SCHED_BURIED})")) self.col.db.execute( - "update cards set %s where queue in (-2, -3)" % self._restoreQueueSnippet) + f"update cards set %s where queue in ({QUEUE_USER_BURIED}, {QUEUE_SCHED_BURIED})" % self._restoreQueueSnippet) def unburyCardsForDeck(self, type="all"): if type == "all": - queue = "queue in (-2, -3)" + queue = f"queue in ({QUEUE_USER_BURIED}, {QUEUE_SCHED_BURIED})" elif type == "manual": - queue = "queue = -3" + queue = f"queue = {QUEUE_SCHED_BURIED}" elif type == "siblings": - queue = "queue = -2" + queue = f"queue = {QUEUE_USER_BURIED}" else: raise Exception("unknown type") @@ -1442,11 +1441,11 @@ def _burySiblings(self, card): rconf = self._revConf(card) buryRev = rconf.get("bury", True) # loop through and remove from queues - for cid,queue in self.col.db.execute(""" + for cid,queue in self.col.db.execute(f""" select id, queue from cards where nid=? and id!=? -and (queue=0 or (queue=2 and due<=?))""", +and (queue={QUEUE_NEW_CRAM} or (queue={QUEUE_REV} and due<=?))""", card.nid, card.id, self.today): - if queue == 2: + if queue == QUEUE_REV: if buryRev: toBury.append(cid) # if bury disabled, we still discard to give same-day spacing @@ -1473,10 +1472,10 @@ def forgetCards(self, ids): "Put cards at the end of the new queue." self.remFromDyn(ids) self.col.db.execute( - "update cards set type=0,queue=0,ivl=0,due=0,odue=0,factor=?" - " where id in "+ids2str(ids), STARTING_FACTOR) + (f"update cards set type={CARD_NEW},queue={QUEUE_NEW_CRAM},ivl=0,due=0,odue=0,factor=?" + " where id in ")+ids2str(ids), STARTING_FACTOR) pmax = self.col.db.scalar( - "select max(due) from cards where type=0") or 0 + f"select max(due) from cards where type={CARD_NEW}") or 0 # takes care of mod + usn self.sortCards(ids, start=pmax+1) self.col.log(ids) @@ -1491,8 +1490,8 @@ def reschedCards(self, ids, imin, imax): d.append(dict(id=id, due=r+t, ivl=max(1, r), mod=mod, usn=self.col.usn(), fact=STARTING_FACTOR)) self.remFromDyn(ids) - self.col.db.executemany(""" -update cards set type=2,queue=2,ivl=:ivl,due=:due,odue=0, + self.col.db.executemany(f""" +update cards set type={CARD_DUE},queue={QUEUE_REV},ivl=:ivl,due=:due,odue=0, usn=:usn,mod=:mod,factor=:fact where id=:id""", d) self.col.log(ids) @@ -1502,11 +1501,11 @@ def resetCards(self, ids): sids = ids2str(ids) # we want to avoid resetting due number of existing new cards on export nonNew = self.col.db.list( - "select id from cards where id in %s and (queue != 0 or type != 0)" + f"select id from cards where id in %s and (queue != {QUEUE_NEW_CRAM} or type != {CARD_NEW})" % sids) # reset all cards self.col.db.execute( - "update cards set reps=0,lapses=0,odid=0,odue=0,queue=0" + f"update cards set reps=0,lapses=0,odid=0,odue=0,queue={QUEUE_NEW_CRAM}" " where id in %s" % sids ) # and forget any non-new cards, changing their due numbers @@ -1540,18 +1539,18 @@ def sortCards(self, cids, start=1, step=1, shuffle=False, shift=False): # shift? if shift: low = self.col.db.scalar( - "select min(due) from cards where due >= ? and type = 0 " + f"select min(due) from cards where due >= ? and type = {CARD_NEW} " "and id not in %s" % scids, start) if low is not None: shiftby = high - low + 1 - self.col.db.execute(""" + self.col.db.execute(f""" update cards set mod=?, usn=?, due=due+? where id not in %s -and due >= ? and queue = 0""" % scids, now, self.col.usn(), shiftby, low) +and due >= ? and queue = {QUEUE_NEW_CRAM}""" % scids, now, self.col.usn(), shiftby, low) # reorder cards d = [] for id, nid in self.col.db.execute( - "select id, nid from cards where type = 0 and id in "+scids): + (f"select id, nid from cards where type = {CARD_NEW} and id in ")+scids): d.append(dict(now=now, due=due[nid], usn=self.col.usn(), cid=id)) self.col.db.executemany( "update cards set due=:due,mod=:now,usn=:usn where id = :cid", d) @@ -1566,7 +1565,7 @@ def orderCards(self, did): def resortConf(self, conf): for did in self.col.decks.didsForConf(conf): - if conf['new']['order'] == 0: + if conf['new']['order'] == NEW_CARDS_RANDOM: self.randomizeCards(did) else: self.orderCards(did) @@ -1584,41 +1583,41 @@ def maybeRandomizeDeck(self, did=None): ########################################################################## def _emptyAllFiltered(self): - self.col.db.execute(""" + self.col.db.execute(f""" update cards set did = odid, queue = (case -when type = 1 then 0 -when type = 3 then 2 +when type = {CARD_LRN} then {QUEUE_NEW_CRAM} +when type = {CARD_FILTERED} then {QUEUE_REV} else type end), type = (case -when type = 1 then 0 -when type = 3 then 2 + when type = {CARD_LRN} then {CARD_NEW} + when type = {CARD_FILTERED} then {CARD_DUE} else type end), -due = odue, odue = 0, odid = 0, usn = ? where odid != 0""", + due = odue, odue = 0, odid = 0, usn = ? where odid != 0""", self.col.usn()) def _removeAllFromLearning(self, schedVer=2): # remove review cards from relearning if schedVer == 1: - self.col.db.execute(""" + self.col.db.execute(f""" update cards set - due = odue, queue = 2, type = 2, mod = %d, usn = %d, odue = 0 - where queue in (1,3) and type in (2, 3) + due = odue, queue = {QUEUE_REV}, type = {CARD_DUE}, mod = %d, usn = %d, odue = 0 + where queue in ({QUEUE_LRN},{QUEUE_DAY_LRN}) and type in ({CARD_DUE}, {CARD_FILTERED}) """ % (intTime(), self.col.usn())) else: - self.col.db.execute(""" + self.col.db.execute(f""" update cards set - due = %d+ivl, queue = 2, type = 2, mod = %d, usn = %d, odue = 0 - where queue in (1,3) and type in (2, 3) + due = %d+ivl, queue = {QUEUE_REV}, type = {CARD_DUE}, mod = %d, usn = %d, odue = 0 + where queue in ({QUEUE_LRN},{QUEUE_DAY_LRN}) and type in ({CARD_DUE}, {CARD_FILTERED}) """ % (self.today, intTime(), self.col.usn())) # remove new cards from learning self.forgetCards(self.col.db.list( - "select id from cards where queue in (1,3)")) + f"select id from cards where queue in ({QUEUE_LRN}, {QUEUE_DAY_LRN})")) # v1 doesn't support buried/suspended (re)learning cards def _resetSuspendedLearning(self): - self.col.db.execute(""" + self.col.db.execute(f""" update cards set type = (case -when type = 1 then 0 -when type in (2, 3) then 2 +when type = {CARD_LRN} then {CARD_NEW} +when type in ({CARD_DUE}, {CARD_FILTERED}) then {CARD_DUE} else type end), due = (case when odue then odue else due end), odue = 0, @@ -1627,7 +1626,7 @@ def _resetSuspendedLearning(self): # no 'manually buried' queue in v1 def _moveManuallyBuried(self): - self.col.db.execute("update cards set queue=-2,mod=%d where queue=-3" % intTime()) + self.col.db.execute(f"update cards set queue={QUEUE_USER_BURIED},mod=%d where queue={QUEUE_SCHED_BURIED}" % intTime()) # adding 'hard' in v2 scheduler means old ease entries need shifting # up or down diff --git a/aqt/browser.py b/aqt/browser.py index f127e0c6eac..c0d71699623 100644 --- a/aqt/browser.py +++ b/aqt/browser.py @@ -252,7 +252,7 @@ def columnData(self, index): t = self.nextDue(c, index) except: t = "" - if c.queue < 0: + if c.queue < 0:#supsended or buried t = "(" + t + ")" return t elif type == "noteCrt": @@ -306,11 +306,13 @@ def answer(self, c): def nextDue(self, c, index): if c.odid: return _("(filtered)") - elif c.queue == 1: + elif c.queue == QUEUE_LRN: date = c.due - elif c.queue == 0 or c.type == 0: + elif c.queue == QUEUE_NEW_CRAM or c.type == CARD_NEW: return str(c.due) - elif c.queue in (2,3) or (c.type == 2 and c.queue < 0): + elif c.queue in (QUEUE_REV, QUEUE_DAY_LRN) or (c.type == CARD_DUE and + c.queue < 0#suspended or buried + ): date = time.time() + ((c.due - self.col.sched.today)*86400) else: return "" @@ -366,7 +368,7 @@ def paint(self, painter, option, index): col = flagColours[c.userFlag()] elif c.note().hasTag("Marked"): col = COLOUR_MARKED - elif c.queue == -1: + elif c.queue == QUEUE_SUSPENDED: col = COLOUR_SUSPENDED if col: brush = QBrush(QColor(col)) @@ -1175,13 +1177,13 @@ def _revlogData(self, cs): _("Resched")][type] import anki.stats as st fmt = "%s" - if type == 0: + if type == CARD_NEW: tstr = fmt % (st.colLearn, tstr) - elif type == 1: + elif type == CARD_LRN: tstr = fmt % (st.colMature, tstr) - elif type == 2: + elif type == CARD_DUE: tstr = fmt % (st.colRelearn, tstr) - elif type == 3: + elif type == CARD_FILTERED: tstr = fmt % (st.colCram, tstr) else: tstr = fmt % ("#000", tstr) @@ -1565,7 +1567,7 @@ def _clearUnusedTags(self): ###################################################################### def isSuspended(self): - return not not (self.card and self.card.queue == -1) + return not not (self.card and self.card.queue == QUEUE_SUSPENDED) def onSuspend(self): self.editor.saveNow(self._onSuspend) @@ -1625,7 +1627,7 @@ def reposition(self): def _reposition(self): cids = self.selectedCards() cids2 = self.col.db.list( - "select id from cards where type = 0 and id in " + ids2str(cids)) + f"select id from cards where type = {CARD_NEW} and id in " + ids2str(cids)) if not cids2: return showInfo(_("Only new cards can be repositioned.")) d = QDialog(self) @@ -1633,7 +1635,7 @@ def _reposition(self): frm = aqt.forms.reposition.Ui_Dialog() frm.setupUi(d) (pmin, pmax) = self.col.db.first( - "select min(due), max(due) from cards where type=0 and odid=0") + f"select min(due), max(due) from cards where type={CARD_NEW} and odid=0") pmin = pmin or 0 pmax = pmax or 0 txt = _("Queue top: %d") % pmin @@ -2095,4 +2097,3 @@ def accept(self): def onHelp(self): openHelp("browsermisc") - From da931b3294cf48ccd39b47107613f244a6955e0b Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Tue, 9 Jul 2019 16:10:27 +0200 Subject: [PATCH 02/68] Correcting parenthesis --- anki/sched.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/anki/sched.py b/anki/sched.py index 4bf10580428..82e7fbf2d22 100644 --- a/anki/sched.py +++ b/anki/sched.py @@ -427,7 +427,7 @@ def totalNewForCurrentDeck(self): f""" select count() from cards where id in ( select id from cards where did in %s and queue = {QUEUE_NEW_CRAM} limit ?)""" - % (ids2str(self.col.decks.active()), self.reportLimit)) + % ids2str(self.col.decks.active()), self.reportLimit) # Learning queues ########################################################################## @@ -785,7 +785,7 @@ def totalRevForCurrentDeck(self): """ select count() from cards where id in ( select id from cards where did in %s and queue = {QUEUE_REV} and due <= ? limit ?)""" - % (ids2str(self.col.decks.active()), self.today, self.reportLimit)) + % ids2str(self.col.decks.active(), self.today, self.reportLimit)) # Answering a review card ########################################################################## @@ -991,8 +991,7 @@ def _dynOrder(self, o, l): elif o == DYN_DUE: t = "c.due" elif o == DYN_DUEPRIORITY: - t = "(case when queue=%d and due <= %d then (ivl / cast(%d-due+0.001 as real)) else 100000+due end)" % (QUEUE_REV, - self.today, self.today) + t = f"(case when queue={QUEUE_REV} and due <= %d then (ivl / cast(%d-due+0.001 as real)) else 100000+due end)" % (self.today, self.today) else: # if we don't understand the term, default to due order t = "c.due" From cebbd6a5d5c01fa126eda656ce180b1ce8a583ed Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Tue, 9 Jul 2019 18:01:12 +0200 Subject: [PATCH 03/68] Adding constants for leech actions --- anki/consts.py | 4 ++++ anki/decks.py | 2 +- anki/sched.py | 2 +- anki/schedv2.py | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/anki/consts.py b/anki/consts.py index 24389208bd4..a2b16e6ce2f 100644 --- a/anki/consts.py +++ b/anki/consts.py @@ -76,6 +76,10 @@ CARD_DUE = 2 CARD_FILTERED = 3 +# Leech actions +LEECH_SUSPEND = 0 +LEECH_TAGONLY = 1 + # Buttons BUTTON_ONE = 1 BUTTON_TWO = 2 diff --git a/anki/decks.py b/anki/decks.py index d692cfd4d31..2165db7f81a 100644 --- a/anki/decks.py +++ b/anki/decks.py @@ -68,7 +68,7 @@ 'minInt': 1, 'leechFails': 8, # type 0=suspend, 1=tagonly - 'leechAction': 0, + 'leechAction': LEECH_SUSPEND, }, 'rev': { 'perDay': 200, diff --git a/anki/sched.py b/anki/sched.py index 82e7fbf2d22..5516dd9d35d 100644 --- a/anki/sched.py +++ b/anki/sched.py @@ -1042,7 +1042,7 @@ def _checkLeech(self, card, conf): f.flush() # handle a = conf['leechAction'] - if a == 0: + if a == LEECH_SUSPEND: # if it has an old due, remove it from cram/relearning if card.odue: card.due = card.odue diff --git a/anki/schedv2.py b/anki/schedv2.py index b0357cfaa5c..ea422d1383e 100644 --- a/anki/schedv2.py +++ b/anki/schedv2.py @@ -1129,7 +1129,7 @@ def _checkLeech(self, card, conf): f.flush() # handle a = conf['leechAction'] - if a == 0: + if a == LEECH_SUSPEND: card.queue = QUEUE_SUSPENDED # notify UI runHook("leech", card) From f77d6a0b0f502750725d676fd53c33672834694c Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Thu, 11 Jul 2019 01:08:22 +0200 Subject: [PATCH 04/68] Colours are considered as constants --- anki/consts.py | 16 ++++++++++++++++ anki/stats.py | 13 +------------ aqt/deckbrowser.py | 5 +++-- aqt/overview.py | 9 +++++---- aqt/reviewer.py | 9 +++++---- 5 files changed, 30 insertions(+), 22 deletions(-) diff --git a/anki/consts.py b/anki/consts.py index a2b16e6ce2f..d813447196e 100644 --- a/anki/consts.py +++ b/anki/consts.py @@ -121,3 +121,19 @@ def dynOrderLabels(): 7: _("Latest added first"), 8: _("Relative overdueness"), } + +# colors +colYoung = "#7c7" +colMature = "#070" +colCum = "rgba(0,0,0,0.9)" +colLearn = "#C35617" +colRelearn = "#c00" +colCram = "#ff0" +colIvl = "#077" +colHour = "#ccc" +colTime = "#770" +colUnseen = "#000" +colSusp = "#ff0" +colNew = "#000099" +colRev = "#007700" +colDue = "#007700" diff --git a/anki/stats.py b/anki/stats.py index eb0fb9cc266..0280558ef55 100644 --- a/anki/stats.py +++ b/anki/stats.py @@ -8,6 +8,7 @@ from anki.utils import fmtTimeSpan, ids2str from anki.lang import _, ngettext +from anki.consts import * # Card stats @@ -86,18 +87,6 @@ def time(self, tm): # Collection stats ########################################################################## -colYoung = "#7c7" -colMature = "#070" -colCum = "rgba(0,0,0,0.9)" -colLearn = "#00F" -colRelearn = "#c00" -colCram = "#ff0" -colIvl = "#077" -colHour = "#ccc" -colTime = "#770" -colUnseen = "#000" -colSusp = "#ff0" - class CollectionStats: def __init__(self, col): diff --git a/aqt/deckbrowser.py b/aqt/deckbrowser.py index f90b6b060d6..8c56d971755 100644 --- a/aqt/deckbrowser.py +++ b/aqt/deckbrowser.py @@ -6,6 +6,7 @@ from aqt.utils import askUser, getOnlyText, openLink, showWarning, shortcut, \ openHelp from anki.utils import ids2str, fmtTimeSpan +from anki.consts import * from anki.errors import DeckRenameError import aqt from anki.sound import clearAudioQueue @@ -190,8 +191,8 @@ def nonzeroColour(cnt, colour): cnt = "1000+" return "%s" % (colour, cnt) buf += "%s%s" % ( - nonzeroColour(due, "#007700"), - nonzeroColour(new, "#000099")) + nonzeroColour(due, colDue), + nonzeroColour(new, colNew)) # options buf += ("" "" % did) diff --git a/aqt/overview.py b/aqt/overview.py index 12c5cd6aee0..9c513fac337 100644 --- a/aqt/overview.py +++ b/aqt/overview.py @@ -6,6 +6,7 @@ import aqt from anki.sound import clearAudioQueue from anki.lang import _ +from anki.consts import * class Overview: "Deck overview." @@ -171,13 +172,13 @@ def _table(self): return '
%s
' % ( self.mw.col.sched.finishedMsg()) else: - return ''' + return f'''
- - - + + +
%s:%s
%s:%s
%s:%s
%s:%s
%s:%s
%s:%s
%s
''' % ( diff --git a/aqt/reviewer.py b/aqt/reviewer.py index f01fcbd9f1a..84c3be95a09 100644 --- a/aqt/reviewer.py +++ b/aqt/reviewer.py @@ -11,6 +11,7 @@ from anki.lang import _, ngettext from aqt.qt import * +from anki.consts import * from anki.utils import stripHTML, bodyClass from anki.hooks import addHook, runHook, runFilter from anki.sound import playFromText, clearAudioQueue, play @@ -522,9 +523,9 @@ def _remaining(self): idx = self.mw.col.sched.countIdx(self.card) counts[idx] = "%s" % (counts[idx]) space = " + " - ctxt = '%s' % counts[0] - ctxt += space + '%s' % counts[1] - ctxt += space + '%s' % counts[2] + ctxt = f'%s' % counts[0] + ctxt += space + f'%s' % counts[1] + ctxt += space + f'%s' % counts[2] return ctxt def _defaultEase(self): @@ -608,7 +609,7 @@ def _contextMenu(self): [_("Replay Own Voice"), "V", self.onReplayRecorded], ] return opts - + def showContextMenu(self): opts = self._contextMenu() m = QMenu(self.mw) From 3cebbe5a088449bdc73df974bc99263c37bbe14b Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Thu, 11 Jul 2019 02:25:50 +0200 Subject: [PATCH 05/68] Correct typo --- anki/sched.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/anki/sched.py b/anki/sched.py index 5516dd9d35d..785d75ea99a 100644 --- a/anki/sched.py +++ b/anki/sched.py @@ -785,7 +785,7 @@ def totalRevForCurrentDeck(self): """ select count() from cards where id in ( select id from cards where did in %s and queue = {QUEUE_REV} and due <= ? limit ?)""" - % ids2str(self.col.decks.active(), self.today, self.reportLimit)) + % ids2str(self.col.decks.active()), self.today, self.reportLimit) # Answering a review card ########################################################################## From 908a3cf727b747bc2d90d11ea09f0e5e75353f08 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Thu, 11 Jul 2019 02:37:00 +0200 Subject: [PATCH 06/68] Numbered custom study buttons --- aqt/customstudy.py | 14 +++++++------- designer/customstudy.ui | 37 +++++++++++++++++++------------------ 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/aqt/customstudy.py b/aqt/customstudy.py index 5dc08f5be4f..616c980b57a 100644 --- a/aqt/customstudy.py +++ b/aqt/customstudy.py @@ -30,17 +30,17 @@ def __init__(self, mw): f.setupUi(self) self.setWindowModality(Qt.WindowModal) self.setupSignals() - f.radio1.click() + f.radioNew.click() self.exec_() def setupSignals(self): f = self.form - f.radio1.clicked.connect(lambda: self.onRadioChange(1)) - f.radio2.clicked.connect(lambda: self.onRadioChange(2)) - f.radio3.clicked.connect(lambda: self.onRadioChange(3)) - f.radio4.clicked.connect(lambda: self.onRadioChange(4)) - f.radio5.clicked.connect(lambda: self.onRadioChange(5)) - f.radio6.clicked.connect(lambda: self.onRadioChange(6)) + f.radioNew.clicked.connect(lambda: self.onRadioChange(RADIO_NEW)) + f.radioRev.clicked.connect(lambda: self.onRadioChange(RADIO_REV)) + f.radioForgot.clicked.connect(lambda: self.onRadioChange(RADIO_FORGOT)) + f.radioAhead.clicked.connect(lambda: self.onRadioChange(RADIO_AHEAD)) + f.radioPreview.clicked.connect(lambda: self.onRadioChange(RADIO_PREVIEW)) + f.radioCram.clicked.connect(lambda: self.onRadioChange(RADIO_CRAM)) def onRadioChange(self, idx): f = self.form; sp = f.spin diff --git a/designer/customstudy.ui b/designer/customstudy.ui index bc677d0e5d5..d3a1f8b1911 100644 --- a/designer/customstudy.ui +++ b/designer/customstudy.ui @@ -6,8 +6,8 @@ 0 0 - 332 - 380 + 351 + 460 @@ -17,42 +17,42 @@ - + Review ahead - + Review forgotten cards - + Increase today's new card limit - + Increase today's review card limit - + Study by card state or tag - + Preview new cards @@ -163,13 +163,14 @@ - radio1 - radio2 - radio3 - radio4 - radio6 + radioNew + radioRev + radioForgot + radioAhead + radioPreview + radioCram spin - buttonBox + cardType @@ -180,8 +181,8 @@ accept() - 248 - 254 + 257 + 427 157 @@ -196,8 +197,8 @@ reject() - 316 - 260 + 321 + 427 286 From e76521a0a588b653fa443cabec431039f87643c5 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Sat, 15 Jun 2019 15:36:06 +0200 Subject: [PATCH 07/68] ignoring some things --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) mode change 100644 => 100755 .gitignore diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 index de80392d529..7dc754c40d1 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,9 @@ aqt/forms locale tools/runanki.system +/anki/ankiweb.certs +/tests/test_sched.py +aqt/buildhash.py +/__main__.py anki/buildhash.py +*autosave From e16bd8c6626ec611de2794f93a8771342f984cb3 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Sat, 15 Jun 2019 15:38:18 +0200 Subject: [PATCH 08/68] dictionnary from name in code and name in manual --- README.name | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100755 README.name diff --git a/README.name b/README.name new file mode 100755 index 00000000000..04220af905a --- /dev/null +++ b/README.name @@ -0,0 +1,8 @@ +Note that some names are distinct in the manual and in the code. + +Code |Manual +---------------------------- +cramming deck |Dynamic deck +model |note type +template |card type +conf/dconf |deck option From 50e21ba8959fae56c33ea9a4723261dcf3cdc439 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Sat, 15 Jun 2019 15:40:36 +0200 Subject: [PATCH 09/68] dictionnary from variable to their meaning --- VARS.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100755 VARS.md diff --git a/VARS.md b/VARS.md new file mode 100755 index 00000000000..9a022c15476 --- /dev/null +++ b/VARS.md @@ -0,0 +1,12 @@ +Variable |Object of class +------------------------------------ +col |Collection +mm |Model Managers +mw |Main window +b |browser +FOOid |Identifier of an object of type foo +n |note +c |card +m |model (note type) +d |deck +pm |profile manager From 78ce41a6a91c247c2246f77000962da8aa31067e Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Sat, 15 Jun 2019 15:40:49 +0200 Subject: [PATCH 10/68] Documenting anki db check --- documentation/computing_intervals.md | 168 ++++++++ documentation/db_check.md | 179 ++++++++ documentation/deletion.md | 67 +++ documentation/due.md | 214 ++++++++++ documentation/glossary.md | 37 ++ documentation/hooks.md | 442 ++++++++++++++++++++ documentation/links.md | 11 + documentation/preview.md | 22 + documentation/profiles.md | 101 +++++ documentation/synchronization.md | 346 +++++++++++++++ documentation/templates_generation_rules.md | 86 ++++ 11 files changed, 1673 insertions(+) create mode 100644 documentation/computing_intervals.md create mode 100644 documentation/db_check.md create mode 100644 documentation/deletion.md create mode 100644 documentation/due.md create mode 100644 documentation/glossary.md create mode 100644 documentation/hooks.md create mode 100644 documentation/links.md create mode 100644 documentation/preview.md create mode 100644 documentation/profiles.md create mode 100644 documentation/synchronization.md create mode 100644 documentation/templates_generation_rules.md diff --git a/documentation/computing_intervals.md b/documentation/computing_intervals.md new file mode 100644 index 00000000000..fbf1476af3f --- /dev/null +++ b/documentation/computing_intervals.md @@ -0,0 +1,168 @@ +# Computing the next review interval + +The manual kind of let you guess how the +[intervals](https://apps.ankiweb.net/docs/manual.html#reviews) are +computed. But actually, you never get the real number, unless you read +the code. So, here is the exact rules. + + +For the new cards (in learning +cards), it is in method `_answerLrncard`. For the review cards, it +is in methods `_answerRevCard` and `_rescheduleRev`. + +## Fuzzyness +Below, we explain how to compute a number of day for the new +interval. Sometime, we'll state that this number is "fuzzied". I.e. it +is slightly increased or decreased, in order to add randomnes, as +[explained in the +manual](https://apps.ankiweb.net/docs/manual.html#what-spaced-repetition-algorithm-does-anki-use). + +We now explain the fuzzy function. The code described here is in +method `_fuzzedIvlRange`. + +* If the interval is 1, then its fuzzy version is 1. +* If the interval is 2, its fuzzy version is 2 or 3. +* If the interval is at least three days, it's fuzzy version is a + number between `interval-fuzz` and `interval+fuzz`. That is, + a few days may be added or removed. We now explain how `fuzz` + is computed. + * If the interval is less than a week, then `fuzz` is 1. + * If the interval is between 7 and 19 days, `fuzz` is 2 + * If the interval is between 20 and 26 days, `fuzz` is 3. + * If the interval is between 27 and 09 days, `fuzz` is 4. + * If the interval is at least a hundrad days, `fuzz` is 5 + percent of the interval. + +## Parameters +We first list the parameters taken into account. + +### Last interval +Last time you reviewed a card, an interval was assigned to it. It is +taken into account to compute the new interval. Unless this is a new +card. + +### Easyness factor +Each card have a parameter called its easyness. It can be seen in the +«ease» factor of the browser. + +The initial easyness, for new cards, depends on the deck. In the +deck's option, in the «New Cards» tag, it's «starting ease». By +default, it's 250%. This number never gets below 130. + +When a review card lapse, it is decreased by 200. If you press hard, +it decreases by 150. If you press easy, it increases by 150. + +For the mathematically inclined person, you can think of easyness as +the second derivative. Indeed the interval (which is the first +derivative) increases quicker when easyness is great. Those numbers, +-200, -150, 0 and 150, would be the four possible jounces, i.e., the +fourth derivatives. + +### interval factor +Each deck option have an interval factor. By default it's 1. You can +edit it in anki. + +### Due date +The date at which you were supposed to see the review card. + +### Delay +If you are late reviewing your card, we also take into account how +late you are. This is called the delay of the card. + +## The formulas +The methods mentionned below appear in `anki.sched.Scheduler`. + +### Filtered deck +If the card is in filtered deck, if the filter option "reschedule +cards based on my answers in this deck" is not checked, then the +interval is not modified and this review is not taken into account. We +now assume that this option is checked + +#### Lapse of filtered +Two cases must be considered, depending on whether the card is seen in +advance or not. + +If you review a card in advance, it's next due date can only +increase. If you lapsed the card, this lapse won't be taken into +account for the due date and interval. Only +success are taken into account. + +If the card was already due, then the new nue date is the next +day. Interval is not modified however. + +(Technically, the due date is the max between the old due date, and +the due date planned by this review) + +#### Successful review of filtered +The new interval is the maximum between old interval and +elapsed*((easyness factor/1000)+1.2)/2, with elapsed being the time +between today and the last review. + +This number is constrained to be between 1 and the `maxIvl` of the +card's configuration. This number is not fuzzified. + +This formula is in the method `_dynIvlBoost`. + +### Standard deck +#### New card +New cards stay in learning mode for some time. It is said to be +graduating when it leaves it. +##### Graduating +This case is quite easy. The initial interval is chosen according to +the values in the deck options. "graduating interval" and "easy +interval". Fuzziness is applied. +##### Remaining in learning mode +Each step is taken according to the deck option. In anki, in the +option, in New Cards, you can find «steps (in minutes)». This give the +list of successive steps. + +#### Review cards +The new interval depends on which buttons is pressed. The main part of +the computation is done in the method `_nextRevIvl`. + +Fuzziness is applied in the three cases. In all three case, the result +is at most maximal interval set in the deck's option. +##### Button Hard +In this case, the new interval is equal to the product of: +* the sum of the interval, and a fourth of the delay +* 1.2 +* the interval factor divided by 100 + +If this number is at most the +last interval, then the new interval is equal to the last interval, +plus one. + +The result is rounded to the lowest integer. + + +##### Button good +In this case, the new interval is equal to the product of: +* the sum of the interval, and the half of the delay +* The easyness factor +* the interval factor divided by 100 + +If this number is at most the one of the button hard, then the new +interval is equal to the hard interval plus one. (This could be the +case if easiness is less than 1.2 and there is no delay) + +##### Button easy +In this case, the new interval is equal to the product of: +* the sum of the interval, and the delay +* The easyness factor +* The easyness factor of the deck. It can be found in the deck's + option, review tag, "easy bonus" +* the interval factor divided by 100 + +If this number is at most the one of the button good, then the new +interval is equal to the good interval plus one. (This could be the +case if you choose a very low easyness for the deck) + + +### Again +This case is quite easy. There is a number of steps, as in the deck's +option's lapse tag. Each time again is pressed, it starts at the first +step. Then the difference of time between two steps is as in this +list. When it graduates again, schedule for the day after. + +When graduating again, the new interval is tomorrow Its done in +`_rescheduleAsRev` diff --git a/documentation/db_check.md b/documentation/db_check.md new file mode 100644 index 00000000000..0f934486463 --- /dev/null +++ b/documentation/db_check.md @@ -0,0 +1,179 @@ +# Database check + +In this document, we consider the processing of checking of the +database. We actually consider three different checkings: +* The one which cause anki to tell that the database has +inconsistencies and that we should use run «Check Database». +* The action which occur when «Check Database» is run. +* The kind of database error which are not repaired nor fixed by anki. +(but they are fixed by the add-on [Database checker/fixer explained, +more fixers](https://ankiweb.net/shared/info/1135180054)). + +## Warning of database inconsistency + +We consider `anki.collection._Collection.basicCheck`. Note that, +contrary to what the name hints, this is not what occurs when you +press «Check Database». Instead, this is run twice during the +synchronization process to check whether the database is still +correct. Once before the sync starts, and once when it ends. Hence, +this is the process which may state that you need to run «Check +Database» because the database is inconsistent. + +This checking does the four following checks: +* whether the model of each note is in the database +* whether the note of each card belong to the database +* whether each note has at least one card +* whether each card's ord is valid. + +## Check Database +Here, we consider what really occur when you press «Check +database». We also explain the meaning of the error message. Note that +if you want to have more details, you should use add-on [Database +checker/fixer explained, more +fixers](https://ankiweb.net/shared/info/1135180054). + +The method described here is `anki.collection._Collection.fixIntegrity`. + +### Checking whether the database is ok +The first test is checking whether sqlite find a problem in the +database. If it is the case, anki prints "Collection is +corrupt. Please see the manual." In this case, it may be hard to +repair, because it may be possible that nothing may be retrieved from +a broken database. + +### note type with a missing model +A note contains a field mid, whose value should be the id of a +model. If there is no such model, nothing can be done of the note, so +it's deleted. (I personnaly believe it makes no sens. The fields and +tags are still quite interesting, and should be shown to the user, so +they know what note to recreate. The note can be found in the file +deleted.txt, but you should read the manual carefully to know this +important fact). + +In this occur, anki prints "Deleted %d card(s) with missing template." +with %d being the number of card deleted. + +### AnkiDroid deck override bug +A card type `t` (a.k.a. template) has a key 'did'. When you create +a new note, the card with this card type are sent to the deck whose id +is `t['did']` if its not None. Its entry may be "None" instead of +None. If it is the case, it is corrected. In this case, Anki prints +"Fixed AnkiDroid deck override bug." + +### Fixing missing req. +A note type (a.k.a. model) has a key 'req', meaning requirements. For +more information about this complex notion, go read [template +generation rules](templates_generation_rules.md). If this key is +missing, it is computed and anki prints "Fixed note type: %s" with %s +the name of the model. + +### Card with invalid ordinal +Each card `c` is associated to a card type. More precisely, to an integer, +which represents the position of the card type in the note type. This +is saved as `c.ord`. If `c.ord` does not correspond to a +position of a card type, this card should not exists. Thus, it is +deleted. Anki prints "Deleted %d card with missing template.", with %d +the number of cards deleted. + +### Notes with invalid field count +Each note has a model and a list of fields. Each model has a list of +field names. Normally, both list have the same size. If it is not the +case, the note is not usable, and is deleted. (As above, I personnally +think that it's a bad thing to do. The fields and tags should be shown +to the user so they decide how to repair the note and keep the +information.) In this card, anki prints "Deleted %d note(s) with wrong +field count." with %d the number of deleted notes. + +### Delete any notes with missing cards +If a note has no card, it becomes useless. It will never be seen +reviewed again. And he can't be edited in the browser. Since nothing +can be done with it, it is deleted. (As far as I'm concerned, this is +one of the most ridiculous thing anki does. For one, as above, you +should warn explictly the user and let them know the fields and tags +which are deleted, so that they can decide whether a new note should +be created with those information. And furthermore, if they edit the +note type, some card may be generated, and the note will have card +!). Anki prints "Deleted %d note(s) with no cards." with %d the number +of note deleted. + +### Cards with missing note +Each card is associated to a unique note. If the note is not in the +database, nothing can be done of the card and it is deleted. Note that +this is different from «Empty cards» action. Indeed, when «Empty card» +is pressed, it is actually check whether the card would be empty or +not, using the value of the field of the note, and the card +type. Without a note, this can not even be tested, since a card does +not contains the fields values. Anki prints "Deleted %d card(s) with +missing note." with %d the number of cards deleted. + +### Cards with odue set when it shouldn't be +If a card is in learning or in the review queue, it seems that the +original due (odue) should be 0. (I must admit it's not clear why odue +is considered, but not due). This set odue to 0 in those cases. Anki +prints "Fixed %d card with invalid properties." with %d the number of +cards changed. + +### Cards with odid set when not in a dyn deck +A card should have a «original deck» (odid) only if it's currently in +a filtered deck (a.k.a. dynamic deck). Thus, if it's not the case, +odid and odue are set to 0. Whether odue is non-zero is not tested. I +assume that it is because having a odue without odid is not actually a +problem. Anki prints "Fixed %d card with invalid properties." with %d +the number of cards with this problem. + +### New cards can't have a due position > 32 bits +For some reason, anki believes that the due value of new cards should +be at most 1,000,000. So if its not the case, the number is changed to +1,000,000. One of the reason to do this is that it ensures that the +number fits on 32 bits. + +Actually, it kind of makes sens. If the card is new, the due value +just give an order in which cards will be seen, and if the first +1,000,000 cards are shown in the correct order, the remaining may be +consireded not important right now. If the card is due, due is the +day in which the card will be seen. When doing this edition, anki +prints nothing. + +### tags +Anki calls self.tags.registerNotes() (todo) + +### nextPos +Anki set the next position value to the successor of the greatest ord +of new card. Note that if this is 1000000, then the nextPos will be a +number so big that it will be decreased during the next fixing of the +database. + +### Reviews should have a reasonable due +TODO. a part of this has been changed since this text was written. below I explain how anki used to deal with due position. + +Similarly to New cards can't have a due position > 32 bits, it is +assumed that the due value of review card (i.e. cards which are not +new anymore, and not in learning) is at most 100000 (there is one zero +less than in the case of new cards.). The due value is the number of +the +day where the card will be seen again for the next time. Computed as a +distance between this day, and the day the collection was +created. Note that 100000 corresponds to 274 years, so it is +reasonnable to assume that this is a number far bigger than what can +actually be usefull for a single user. + +During those edition, anki prints nothing. + +### Rounding decimal values +V2 scheduler used to have a bug which could create decimal interval +and due date for some reviewed cards. Those value should be +integers. Thus anki correct this by rounding those value. If this is +the case, anki prints "Fixed %d cards with v2 scheduler bug." with %d +the number of cards affected. + +### Optimizing the database +Anki calls sqlite3 and tells it to optimize itself. + + +## Errors not taken into account (yet) by anki. + +### Multiple instance of the same card type of the same note +In anki database, in the table cards, the pair (nid,ord) should be +unique. I.e. a note may have at most one card of each card type. This +is actually never checked. And it is a problem I reall had. probably +because of another add-on. diff --git a/documentation/deletion.md b/documentation/deletion.md new file mode 100644 index 00000000000..0eee896616f --- /dev/null +++ b/documentation/deletion.md @@ -0,0 +1,67 @@ +# Deletion +Deleting cards and notes seems to be a simple notion. Actually, there +are plenty of cases which are not straightforward and which needs to +be taken into account. + +## Deleting a note directly + +This is the simplest case. If you select a card in the browser, or is +reviewing a card, and you decide to delete it, you'll delete the +entire note. This is also what occurs if you delete a note type. (Note +that if you have in your collection a note whose note type is unkwown, +or with a wrong number of field, or which has no card, it will also be +silently deleted during a database check.) + +## Deleting cards +Here are the following way to delete a card. +* deleting a deck +* deletion requested by a synchronization. + +It should be noted that, in those two cases, if the note still +exists and the card is not empty, the next «check database» will +regenerate those cards, but they'll be considered to be brand new +cards. + +* changing a note's note type. + +You change use «change the note type» without actually changing it, +and just moving a card to «nothing». Note however that, if this card +is not empty, it will immediatly be regenerated. Thus what you are +doing is only changing this card for a new card (and potentially +changing it's deck, because anki may have to guess the deck for the +new card.) + +* deleting a card type (this can not be done if this is the card type + of the only card of a note) +* deleting the note containing the card +* deleting empty cards + +Of course this last option only delete empty cards. + +* doing a database check +this delete cards if the card has a position which is incorrect, or if +it's note does not exists anymore. + + +## Deleting each card of a note +By default, if each card of a note are deleted, then the note is also +deleted. + +There is one exception of this rule: when the card suppresion is due +to a synchronization. In this case, the note may stay in your +collection, with no card. I believe that the reason for this choice is +that some cards may be downloaded later in the synchronization for +this note, thus this note should be keep. And anyway, if there are +really no card at all, then the note should also be deleted in the server, +and thus the synchronization will eventually request the deletion of +the note. (See [synchronization.md] for more details) + + + +This rules sometimes make sens. If you delete a deck, you probably don't +expect it's card to respawn later in another deck as new card. Except +that it also means that if you did a mistake in a note template, you +may lose every note when you click on «Empty cards...». And while it +may be acceptable to lose cards (especially if they are all new), I +believe that losing a note is a huge problem, because you can't +recreate them automatically. diff --git a/documentation/due.md b/documentation/due.md new file mode 100644 index 00000000000..3062d27f968 --- /dev/null +++ b/documentation/due.md @@ -0,0 +1,214 @@ +This document discuss the `due` value of new card. It's main goal +is to express why I believe it to be currently broken. I will give +plenty of way to have wrong `due` values, without even installing an +add-on. + +It also propose ways to fix it, and explain the problem I see with all +of them. Which, I believe, argues that the definition of `due` should be +slightly changed. + +In this document, I will only consider new cards, but I will not +repeat it again. + +# Definition +Because some terms are confusing, I have to state some explicit +definition now. + +## New card. +When I write «new card» in this document, it always mean a card which +has the property to be «new». It may be a card created years ago. When +I speak of a card which has just been created, I'll write «the created +card». + +## What is `due` ? +New cards are shown in a particular order. Computing this order may +takes some time. So, instead, anki pre-compute this order. This +precomputation is called `due`. Each new card has an integer value +called `due`. The cards which should be seen first have the lowest +`due` value. + +The `due` value is a 32 bit integer. This is ensured because each time +a `due` value is greater than 1,000,000, it is changed to 1,000,000 +during database check. + +## Reordering a deck +I'll speak a lot about "reordering a deck", so let me introduce this +notion here. By «reordering a deck», I mean going to the deck option +and changing «new card in random order» to «new cards in order added» +and reciprocally. This action ensure that each card of the deck get a +new `due` value, almost according to the description of the option +(but not totally). + + +## Properties that the `due` value should hold +### Siblings +For the sake of the simplicity, let us assume in this section +(Siblings) that sibling cards belong to the same deck. + +Ideally, if you have seen a new card, you'll see its sibling very +soon. In the best case, as soon as you see a new card from the same +deck, you'll see the new siblings of any card you have already seen, +unless those cards are already buried/suspended. You'll discover a new +note only if no such new siblings exists. This certainly may help to +remember the whole content of the note. + +This is ensured by giving the same `due` date to all siblings. So all +new card with the smallest `due` date are siblings. However when you +reorder a deck, and set it to random order, a new card may have a high +`due` value and siblings which are not new. This means that you'll +have to wait quite some time to see a new sibling of a seen card. + + +The property that all siblings have the same `due` date is true while +notes are created. If each sibling of a card belong to the same deck +(not considering subdecks), and this deck is reordered (i.e. in deck's +option, the new card in random order/order added) is toggled. + +However, this property may get lost in at least two cases: +* if a card is reordered while its sibling is not +* if siblings belong to different deck, and one deck gets reordered + +### Uniqueness +Normally, each card with the same `due` value belongs to the same +note. This is ensured because each time a `due` value is required, +(i.e. when adding a card or reordering a deck), the `due` value +chosen are greater than the maximal `due` value already present in +the collection. + +This property is true almost all the time, at least unless you or an +add-on change the database directly. However, there is one case where +this became false, if the `due` value of a card is 1,000,000 or +greater. In this case: +* until april 2019 (anki 2.1.12): all cards have `due` value 1,000,000. +* since april 2019 (anki 2.1.13): the due values are computed modulo + 1,000,000, which means that you may have a lot of cards with due + value 0, 1, ... + +### Order of adding cards +If a deck has «new card in order added», then the `due` card +represents the order in which card are added to the collection. When +cards are created using anki directly, or imported from a CSV file, it +simply means that the `due` are in the same order than the `created` +value. However, if a deck is imported (for example by downloading it +from ankiweb first), then the `created` value represents the day where +the card was created originally, and not the day were the cards were +added to our collection. + +In particular, it means that the «order added» can never be recomputed +once it is lost. This can be seen using the following steps: +* take a deck with «new card in random order». +* add it a new note +* import a deck (which was created before your new note was created) +* toggle the deck to new card in order added. + +You'll see that the imported notes are shown before the note you +created, while they were added after it. + +### Random order +If a deck has «new cards in random order», the notes should be seen in +an unpredictable order. + +Actually, this occurs only in two cases: +* when the notes are imported in the deck from an anki2 file, +* for cards present in the deck when the deck has been reordered, + +## How to reproduce bugs +In this section, I explain how to have a few strange result, without +using any add-on. The argument here is not that those bug occurs +often. It just illustrate that something may be quite wrong when +things as simple as changing the deck of a card occurs. + +### Random deck not actually random +Create an empty deck, set it to «new cards in random order». Then add +10 notes. Review this deck. You'll see card in order added. The +problem here is that cards created are not randomized, only card +present in the deck when the deck option was changed. + +### How to obtain an arbitrary order in any deck +Create 10 decks, with 10 decks option. In each deck, put a card. Then +reorder each deck. Finally, put the 10 cards in the same deck. If you +review this deck, you'll see the card in the order in which you have +reordered deck. This is far from being the «order added» nor a «random +order». + +### Creating cards of an already existing note +A card may be created for a note already existing. This may occur in +at least two ways. By adding text in an empty field, or by editing the +note type (either changing a card type already existing, or adding a +new card type). + +This may have two distinct effect, depending on whether this note +still have a new card. If a note has a new card, the `due` value of the +created new card is the same as the one of the new card(s) already +existing. However, if no such new card existed, then the `due` value of +the created card is higher than all other `due` value in the collection. + +Both action have sens, depending on whether you want to consider when +notes are added, or when cards are added. But the fact that the `due` +value is so different depending on this property is clearly not what +is expected. + +### `Due` 1,000,000 +As explained above, the `due` of a card is a 32 bits integer. To +ensure that it remains so, when database is checked, every `due` +greater than 1,000,00 is: +* until april 2019 (anki 2.1.12): changed to 1,000,000 . This mean + that as soon as a single card has value 1,000,000, all new cards + have this `due` value. Thus `due` becomes totally useless. +* since april 2019 (anki 2.1.13): changed to their value modulo + 1,000,000. Thus the last card added will have due value 0, 1, + ... and will become the first card to be reviewed. + +According to +(lovac42)[https://anki.tenderapp.com/discussions/ankidesktop/33664-due-value-of-new-card-being-1000000#comment_47198513], +at least 3 of the 10 random deck he checked has `due` value greater than +1,000,000. This means that, each time a user download such a deck, +there is a high risk that this very problem occurs. Worst, if this +user share a deck later, this bug will propagate. + + + +## Possible solution +### Order computed +The first thing which, I believe, should be done is to stop calling +«order added» and use instead «order created». This is quite +important, because that the only think which can actually be computed +by anki. + +### No `due` for new cards except for random order +When you want to select a card in a deck and you want to show cards in +order created, then you can use a sql query which takes new card with +minimal `(id,ord)`. This is as easy as finding minimal `due`, and it +will always work. + +### Select siblings for random decks +In a deck in which new cards are selected in random order, select new +cards with have a non-new sibling first. This can be done by having a +binary flag stating whether there is a non-new sibling. This is non +trivial to code, because this flag may have to change often, but it's +not computationally hard. + +### Recompute `due` in decks more often +As explained above, as soon as new card are added in a random deck, +and as soon as as card move to another deck, the `due` value may become +wrong. Thus I believe that, if `due` is kept, it should be recomputed +when the deck is changed. No need to recompute it each time a card is +added. Just mark the deck as needed recomputation, and do the +recomputation when new cards are reviewed. Note that, this partially +defeat the purpose of preprocessing. + +Note also that this is what does the add-on [https://github.com/Arthur-Milchior/anki-correct-`due`] + +### Do a recomputation of all due values +If database check realize that some `due` value is 1,000,000, then +all due value should be recomputed. As long as there are less than +1,000,000 new card, the order may be kept, while keeping small values. + +As explained above, I highly doubt that keeping the current `due` +order is important, because they have almost no meaning +currently. However, I should note that, this can't simply be +implemented using `col.sched.sortCards` as proposed by +(lovac42)[https://github.com/Arthur-Milchior/anki-correct-due/issues/1]. Indeed, +this method gives the same `due` value to each card of a note, +while different card may have different `due` values, as explained +above. It means that this method may change the order of some new cards. diff --git a/documentation/glossary.md b/documentation/glossary.md new file mode 100644 index 00000000000..63dcf2b7014 --- /dev/null +++ b/documentation/glossary.md @@ -0,0 +1,37 @@ +Here is the documentation of vocabulary found in anki code and not in +anki's documentation. Because it's mostly internal stuff. + +# Collection +## Deck conf +That's one of the option which can be applied to a deck in the main +page. + +## Model +What code calls model is called «note type» in Anki's +documentation. The name model is still found in the synchronization +protocol, and in «mid», which represents id of note type. + +## Template +What code calls template is called «card type» in Anki's +documentation. + +## id's +The idea of an object is often denoted as `Oid`, with O the +initial of the kind of object. Thus cid, nid, mid, did and oid, for +ids of card, note, model, deck, and original deck. + +However, in the object itself, the id is denoted as `id` and not +as `Oid`. For example, the id of a card is `card.id`. + + +# Main window +The main window may display different kind of content. The «state» of +the main window is the kind of content on it. + +* Reviewer: The window with a card, and button to answer. +* DeckBrowser: The list of decks +* sync: waiting for sync to finish (the content is the same as + deckbrowser) +* overview: what you obtain when selecting a deck but not yet studying it +* profileManager: no window, instead ask to choose for a profile +* startup: empty window, before collection is loaded. diff --git a/documentation/hooks.md b/documentation/hooks.md new file mode 100644 index 00000000000..8042fef62d1 --- /dev/null +++ b/documentation/hooks.md @@ -0,0 +1,442 @@ +# All hooks in anki. +This document contains a description of all hooks in Anki at time of +writting. + +Each description contains the argument the hook expect. A description +of when the hook is called. And which functions are in the +hook. Either in anki's code. Or in some add-ons, which may serve as +example. + +## Hooks +Some hooks are similar. For examples, hooks used to add action to +context menu (menu opened by right clicking). Thus, I sort the hooks +by similarity. + +### Menues +In this section, we consider menues. Either obtained by doing a right +click. Or by the top-bar menu. + +Unless specified otherwise, no functions are added to the hook. + +#### Top menu +##### browser.setupMenus +Called from browser when menus are created. + +#### Context menues +A context menu is obtained by right-clicking somewhere. + +Unless specified otherwise, those hooks takes two arguments: +* self: the window or subwindow on which a right click is done +* m: a qmenu object which will be shown near to the click. In which + more actions can be added. + +##### AnkiWebView.contextMenuEvent +It allows to add actions to do by right clicking in any html window +using aqt.webview. + +Called in aqt.webview.AnkiWebView.contextMenuEvent + +##### browser.onContextMenu +Called when right clicking in the columns of the browser. + +Called in aqt.browser.Browser.onContextMenu. + +##### EditorWebView.contextMenuEvent +Called from aqt.editor.EditorWebView.contextMenuEvent, the part of the +window used to edit note. + +##### Reviewer.contextMenuEvent +Called from aqt.reviewer.Reviewer.showContextMenu: the class for the +reviewer of cards in the main window + +#### Other menues +###### setupEditorButtons +Called from aqt.editor.Editor.setupWeb, with the buttons on right top, +and the editor itself. It would allow to add more buttons on the top +right of editors + +##### mungeEditingFontName +Contains aqt.editor.fontMungeHack, which return the input, by +replacing the end of the string, if it's " L" by " Light". Because of +some difference between name in QFont and WebEngine. + +Called in aqt.editor.Editor.fonts, when the list of filters is generated. + +##### showDeckOptions +Called from aqt.deckbrowser.DeckBrowser._showOptions. Allow to add +actions to do to list of decks, after Rename, options, export, and +delete. + +For example, add-on [Change option +recursively](https://ankiweb.net/shared/info/751420631) uses it to add +the way to change option of deck and subdecks. + +##### AddCards.onHistory +Called from aqt.addcards.AddCards.onHistory, i.e. when you click on +the History button of the "add card" window. It allow mostly to add +more cards, for example in addon [Open 'Added Today' from +Reviewer](https://ankiweb.net/shared/info/861864770). + +### State changes +The hooks in this section are called when something change. + +##### exportersList +Arguments is: +* exps: a list of exporter (i.e. class implementing + anki.exporting.Exporter. ) + +It is called in anki.exporting.exporters(), a function which returns +the list of exporters existing in the code. It allows to add (or +remove ?) exporters, and thus to export in other file format. Thus it +allows to add other element in the export window. + +#### Model Change +##### odueInvalid +Contains anki.main.AnkiQt.onOdueInvalid, which show a warning and ask +to check the database. + +Called from anki.cards.Cards.flush and .flushSched if a card's queue +is rev, has an odue value but is not in a dynamic deck. + +##### afterStateChange +##### beforeStateChange +It's called just after and just before the main window's state +changed. + +Arguments are: state, oldState, *args + +See aqt.main's documentation to know which are the potential states. + +##### currentModelChanged +Called when the note type (a.k.a. model) is changed. It's run only +from modelchooser. It the windows importdialog, deckchooser, +changemodel and addcards are opened/closed, the following methods are +added/removed from this hook: aqt.importing.ImportDialog.modelChanged, +aqt.deckchooser.DeckChooser.onModelChange, +aqt.browser.ChangeModel.onReset and +aqt.addcards.AddCards.onModelChange. + +##### reset +Contains the method self.onReset, where self is the class +aqt.addcards.AddCards, aqt.modelchooser.ModelChooser, +aqt.studydeck.StudyDeck, aqt.browser.Browser, aqt.browser.ChangeModel, +aqt.main.AnkiQt, aqt.editcurrent.EditCurrent, while those windows are +opened. This function is essentially similar to reopening the window, +setting everything according to the current state of the collection. + +It is called from aqt.AnkiQt.reset, i.e. when the collection should be +reseted, if a collection is loaded. This is called often, when +anything occurs to the collection. + +##### leech +Contains anki.reviewer.onLeech. Which state that a card is a leech, +and maybe that it is suspended. It also contains function for tests +(not considered in this documentation). + +It is called from both scheduler, when a card become a leech. + +##### remNotes +Contains aqt.main.AnkiQt.onRemNotes, a method adding a line about +deletion in the file deleted.txt. The argument is the collection and +the ids of the removed notes. + +It is called from anki.collection._Collection._remNotes. + +##### newDeck +Called from anki.decks.DeckManager.id when a new deck is created. + +Contains aqt.browser.Browser.maybeRefreshSidebar (if the browser is +on), which change the side bar of the browser if it is visible, to +show the new deck. + +##### newModel +Called from anki.ModelManager.models.save each time note type +(a.k.a. models) are saved (even if the model is not new) + +Contains aqt.browser.Browser.maybeRefreshSidebar (if the browser is +on), which change the side bar of the browser if it is visible, to +show the new deck. + +##### modSchema +Contains aqt.main.AnkiQt.onSchemaMod, which ask the used to confirm +they accept a change requiring to upload the full database. + +Is called as a filter in anki.collection._Collection.modSchema, with +parameter True. If it returns False, i.e. the user cancels, then an +error is raised, aborting the schema modification. + +##### newTag +Called from anki.tags.TagManager.register(tags) if at least one of the +tag of tags is new. + +Contains aqt.browser.Browser.maybeRefreshSidebar (if the browser is +on), which change the side bar of the browser if it is visible, to +show the new deck. + +#### GUI change +###### editFocusLost +contains aqt.browser.Browser.refreshCurrentCardFilter, +i.e. aqt.browser.Browser.refreshCurrentCard. Hence refresh the note in +the dataModel and (re)render the preview. + +Called from aqt.editor.Editor.onBridgeCmd when the command starts by +"Blur" (I have no idea what it means) + +##### setupEditorShortcuts +Arguments are: cuts, self + +Called from aqt.editor.Editor.setupShortcuts, i.e. when the note +editor is shown somewhere. + +The two arguments are the list of shortcuts, and the editor. This +allow to add more shortcut to the editor. + +##### self.state+"StateShortcuts" +Called from aqt.main.AnkiQt.setStateShortcuts. I.e. for +aqt.reviewer.Reviewer.show and aqt.overview.Overview.show. + +The argument is the list of shortcuts for the current states. It +allows to add more shortcut to this state. + +###### setupStyle +Called from aqt.main.AnkiQt.setupStyle. Hence during +aqt.main.AnkiQt.setupUI. Takes as argument the buffer, as a string,, +with its QMenuBar and QTleeWidget. Not clear exactly what it does, +because I don't know graphical user interface. + +##### profileLoaded +Called in anki.main.AnkiQt.loadProfile, after the end of the loading, +before calling the onsuccess function given in argument. Note that the +collection is not yet accessible from the main window. + +An add-on used this to ensure that some event is executed after +profile is loaded, and not at start-up as is the default. + +##### unloadProfile +Called anki.main.AnkiQt.unloadProfile, i.e. when anki exits, when +there is a change of profile. It contains anki.sound.stopMplayer, +which stop the music player. + +##### tagsUpdated +Called from aqt.editor.Editor.saveTags, with as argument the note +contained in the editor. Hence when the note should be saved. + +##### browser.rowChanged +Called when the selected card(s) are changed in the browser. After the +change is processed, but before rendering the view. + +##### loadNote +It's called from aqt.editor.Editor.loadNote.oncallback, when a note is +loaded in the editor (i.e. the bottom part appearing in the browser +when a single card is selected). + +Contains aqt.browser.Browser.onLoadNote while the browser is +open. Takes as argument the editor. This function refresh the current +card, using the note in the editor. + +##### undoState: +Contains aqt.browser.Browser.onUndoState(on) if a browser is +opened. This method turn the undo action on or off according to the +Boolean value of `on`. It change the text of this action. + +It is called from aqt.main.AnkiQt.maybeEnableUndo, which change the +undo action from the main window. + +##### showAnswer +Called at the end of `aqt.reviewer.Reviewer._showAnswer`. + +##### showQuestion +Called at the end of `aqt.reviewer.Reviewer._showQuestion`. + +### Unknown +Those hooks are called in a part of code I don't understand yet. Help +is welcomed. + +##### mpvWillPlay +Contains aqt.main.AnkiQt.onMpvWillPlay, which seems to change the +_activeWindowOnPlay to some window, unless the argument file is a video. + +Called from anki.sound.MpvManager.queueFile(file), with the file as +argument. This method is called while anki.sound.setupMpv, from +anki.main.AnkiQt.setupSound, from anki.main.AnkiQt.setupUI, from +anki.main.AnkiQt.__init__. Hence it seems to be called only once. + +##### editFocusGained +Arguments are: self.note, self.currentField + +##### editTimer +Arguments are: self.note + +Contains aqt.browser.Browser.refreshCurrentCard when the browser is open. +##### exportedMediaFiles +Arguments are: c: a path + +Contains aqt.exporting.ExportDialog.exportedMedia but only while +aqt.exporting.ExportDialog.accept runs, while it calls +aqt.exporting.ExportDialog.exporter.exportInto where +aqt.exporting.ExportDialog.exporter is some class of anki.exporting. + +Called from anki.AnkiPackageExporter._exportMedia each time a media is +exported. + +##### noteChanged +Called from anki.main.AnkiQt.noteChanged(nid), with argument +nid. Seems to never be called. + +### Network +##### httpRecv + +Contains a function recvEvent while some download occur. The hook is +removed when the download ended. + +The download may be either an add-on, or a synchronization. + +This method takes as argument a number of byt, add it to some count +`self.recvTotal` and then emitting something as a QT object. In +the case of synchronization, if the sync is aborted, it raises an +exception. + +It is called from anki.sync.AnkiRequestsClient.streamcontent when a +chink of data is received. This data may be either an add-on or a synchronization. + +##### httpSend +Similar to httpRecv, but for emissiond of synchronization, and not for +reception. + +Called from anki.sync._MonitoringFile.read. + +##### sync +Contains, equivalently: +* (lambda type:aqt.sync.SyncThread.fireEvent("sync",type)), +* (lambda type:aqt.sync.SyncThread.event.emit("sync",type)) +* (lambda type:aqt.sync.onEvent("sync",type)) +but only while aqt.sync.SyncThread.run runs, i.e. while anki synchronization is +running. Here, event is a pyqtSignal(str, str) + +It is called from anki.sync.Syncer.sync, each time the +synchronization's step change. The different step/type are, in this +order: +* login: just after saving the collection +* meta: startup and deletions +* server: stream large tables from server +* client: stream to server +* sanity: sanity check +* finalize + +Furthermore, there is the step/type "stream" called while streaming +occur in steps Server and client. + +It's not clear what is the goal of this, since the output of the hook +is never used, and the function does not have any access to the +synchronizer object. + +It's also called from anki.sync.FullSyncer.download, with the +following type: +* download +* upgradeRequired +and from anki.sync.FullSyncer.uplad, with the types: +* upload + +And from anki.sync.MediaSyncer.sync, with: +* findMedia. + +##### syncMsg +Contains aqt.sync.SyncThread.run.syncMsg, hence (lambda +msg:aqt.sync.SyncThread.fireEvent("syncMsg",msg)), hence (lambda +msg:aqt.sync.SyncThread.event.emit("syncMsg",msg)), where event is a +pyqtSignal(str, str). + +It contains this function only while the synchronization thread +runs. + +It's called from anki.sync.MediaSyncer.sync with a text stating how +many media remains to upload. And from +anki.sync.MediaSyncer._downloadFiles, where it states how many media +remains to download. + + +### Adding options +##### search +called from anki.find.Finder.__init__, with the argument +anki.find.Finder.search. This is a dictionnary from terms which can be +used in a search (in browser, in filtered deck) to a function giving a +sql query satisfying this search. This allow to add new terms and +function to this list. + +### fmod +The following four filters are called from +anki.template.template.Template.render_unescaped. When a field start +by `{{foo:`, i.e. it is `{{foo:bar}}`, the filter fmod_foo is +called on the content of the field bar. This may occur multiple time, +since multiple modifier may begin a mustache. + +For example, the add-on [Edit Field During +Review](https://ankiweb.net/shared/info/1549412677) add a hook +fmod_edit, so that you can use `{{edit:` in card type. + +##### fmod_kanji +##### fmod_kana +##### fmod_furigana +Contains methods anki.template.kanji, .kana, and .furigana. This edit +the text given in argument. Replacing   by spaces. And then some +transformation which I don't understand. It's supposed to be for Japan language. + +##### fmod_hint +Contains anki.template.hint.hint. A method which generate html for +hints link in questions. And the js code which replace the hint +link by the hint. + + + +#### Hooks called, but from methods which are never called themselves. +##### colLoading +Arguments are: self.col +It seems to be never called. More precisely, it's called in +aqt.main.AnkiQt._colLoadingState, which itself is never called. +##### mpvIdleHook +Contains aqt.main.AnkiQt.onMpvIdle. Not clear yet what it does. + +Called from anki.sound.MpvManager.on_idle, which seems to be never +called. + +##### reviewCleanup +Called from aqt.reviewer.Reviewer.cleanup, hence from +anki.main.AnkiQt._reviewCleanup assuming the new state is neither +"resetRequired" nor "review". Which itself seems to never be called. + + + +### Preparing question and answers +##### mungeQA +Contains anki.latex.mungeQA. A method which generate HTML which show +an image which contains the compilation result of the LaTeX given in input. + +Called from anki.collection._Collection._renderQA + +###### prepareQA +Called from aqt.reviewer.Reviewer._showQuestion, with the text of the +question, the card object and the text "reviewQuestion". + +Called from aqt.reviewer.Reviewer._showAnswer, with the text of the +answer, the card object and the text "reviewAnswer". + +Called from aqt.clayout.Reviewer._renderPreview, with the text of the +question, the card object and the text "reviewQuestion". + +Called from aqt.clayout.Reviewer._renderPreview, with the text of the +answer, the card object and the text "reviewAnswer". + +###### mungeFields +Called from aqt.collection._Collection._renderQA, after {{type: are +processed, wrapping a template in `{{^Field}}` will not do what you expect. + +But it let us guessing which are the cases creating trouble and which +cases are ok. + +This document explains which are the rules which decides when a card +is generated or not. Those rules are implemented in +[models.py](../anki/models.py), in the method `_reqForTemplate`. + +I assume that those rules are created in order to make anki quicker, +and less expensive in computation time. Alas, it makes anki more +complicated for power user. + +## The rules + +### Non cloze models + +Anki generate the html content of some cards in some cases. It checks +this content to choose what kind of rules should be applied + +First, anki generates the html of card where all fields are +empty. We'll call this content `empty content`. We'll use this +twice below. + + +#### Everything and Nothing +Anki generates the content where every fields contains "ankiflag". It +then checks whether the result is equal to `empty content`. +Intuitively, if you have the same result when everything is filled and +when everything is empty, it probably means that the template does not +consider its input. Anki then consider that this template can be +discarded. + +In this case, the method `_reqForTemplate` returns `("none",[],[])`. + +This is why the documentation state that you should not put everything +inside `{{^Field}}...{{/Field}}`. If you do that, then each time +the field `Field` is filled, the html content of this card is +empty. And thus anki believes that the note never generate any +content. + +In particular it means that no card with this note type will ever be +generated. + +### Removing one field +Now we know that some fields are actually used in the template, +and that, if every fields are filled, we have some content. Now, we +consider what happens if a single field is missing. + +Thus, we generate the html content, when a field `Field` is empty, +and every other field contains the text "ankiflag". If we also find +"ankiflag" in the result, it means a field was shown, thus `Field` +is not mandatory. + +If we don't find "ankiflag" in the result, we consider `Field` to +be mandatory. + +If there is at least one mandatory field, `_reqForTemplate` +returns the pair `("all",l)` where `l` is the list of +mandatory flags. + +In this case, cards are generated for this template if and only if all +of those "mandatory" fields are filled. + +### Using a single field +We now assume that no fields are mandatory. In this case we check for +fields which are sufficient by themselves to generate a card with some +content. + + +Thus, we generate the content, when a field `Field` contains "1", +and every other fields is empty. If the result is not the same as the +html `empty content` computed above, then we consider that the +field `Field` is sufficient to generate the card. + +Then `_reqForTemplate` returns `("any",l)` where `l` is +the list of sufficient field. Note that the list may be empty. + +In this case, cards are generated for this template if and only if all +of those "mandatory" fields are filled. From 39efbc2843d00e4aec612cd0c7b87711375e153f Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Sat, 15 Jun 2019 15:42:16 +0200 Subject: [PATCH 11/68] describing how schd v1 computes interval --- documentation/deletion.md | 67 --- documentation/due.md | 214 ---------- documentation/glossary.md | 37 -- documentation/hooks.md | 442 -------------------- documentation/links.md | 11 - documentation/preview.md | 22 - documentation/profiles.md | 101 ----- documentation/synchronization.md | 346 --------------- documentation/templates_generation_rules.md | 86 ---- 9 files changed, 1326 deletions(-) delete mode 100644 documentation/deletion.md delete mode 100644 documentation/due.md delete mode 100644 documentation/glossary.md delete mode 100644 documentation/hooks.md delete mode 100644 documentation/links.md delete mode 100644 documentation/preview.md delete mode 100644 documentation/profiles.md delete mode 100644 documentation/synchronization.md delete mode 100644 documentation/templates_generation_rules.md diff --git a/documentation/deletion.md b/documentation/deletion.md deleted file mode 100644 index 0eee896616f..00000000000 --- a/documentation/deletion.md +++ /dev/null @@ -1,67 +0,0 @@ -# Deletion -Deleting cards and notes seems to be a simple notion. Actually, there -are plenty of cases which are not straightforward and which needs to -be taken into account. - -## Deleting a note directly - -This is the simplest case. If you select a card in the browser, or is -reviewing a card, and you decide to delete it, you'll delete the -entire note. This is also what occurs if you delete a note type. (Note -that if you have in your collection a note whose note type is unkwown, -or with a wrong number of field, or which has no card, it will also be -silently deleted during a database check.) - -## Deleting cards -Here are the following way to delete a card. -* deleting a deck -* deletion requested by a synchronization. - -It should be noted that, in those two cases, if the note still -exists and the card is not empty, the next «check database» will -regenerate those cards, but they'll be considered to be brand new -cards. - -* changing a note's note type. - -You change use «change the note type» without actually changing it, -and just moving a card to «nothing». Note however that, if this card -is not empty, it will immediatly be regenerated. Thus what you are -doing is only changing this card for a new card (and potentially -changing it's deck, because anki may have to guess the deck for the -new card.) - -* deleting a card type (this can not be done if this is the card type - of the only card of a note) -* deleting the note containing the card -* deleting empty cards - -Of course this last option only delete empty cards. - -* doing a database check -this delete cards if the card has a position which is incorrect, or if -it's note does not exists anymore. - - -## Deleting each card of a note -By default, if each card of a note are deleted, then the note is also -deleted. - -There is one exception of this rule: when the card suppresion is due -to a synchronization. In this case, the note may stay in your -collection, with no card. I believe that the reason for this choice is -that some cards may be downloaded later in the synchronization for -this note, thus this note should be keep. And anyway, if there are -really no card at all, then the note should also be deleted in the server, -and thus the synchronization will eventually request the deletion of -the note. (See [synchronization.md] for more details) - - - -This rules sometimes make sens. If you delete a deck, you probably don't -expect it's card to respawn later in another deck as new card. Except -that it also means that if you did a mistake in a note template, you -may lose every note when you click on «Empty cards...». And while it -may be acceptable to lose cards (especially if they are all new), I -believe that losing a note is a huge problem, because you can't -recreate them automatically. diff --git a/documentation/due.md b/documentation/due.md deleted file mode 100644 index 3062d27f968..00000000000 --- a/documentation/due.md +++ /dev/null @@ -1,214 +0,0 @@ -This document discuss the `due` value of new card. It's main goal -is to express why I believe it to be currently broken. I will give -plenty of way to have wrong `due` values, without even installing an -add-on. - -It also propose ways to fix it, and explain the problem I see with all -of them. Which, I believe, argues that the definition of `due` should be -slightly changed. - -In this document, I will only consider new cards, but I will not -repeat it again. - -# Definition -Because some terms are confusing, I have to state some explicit -definition now. - -## New card. -When I write «new card» in this document, it always mean a card which -has the property to be «new». It may be a card created years ago. When -I speak of a card which has just been created, I'll write «the created -card». - -## What is `due` ? -New cards are shown in a particular order. Computing this order may -takes some time. So, instead, anki pre-compute this order. This -precomputation is called `due`. Each new card has an integer value -called `due`. The cards which should be seen first have the lowest -`due` value. - -The `due` value is a 32 bit integer. This is ensured because each time -a `due` value is greater than 1,000,000, it is changed to 1,000,000 -during database check. - -## Reordering a deck -I'll speak a lot about "reordering a deck", so let me introduce this -notion here. By «reordering a deck», I mean going to the deck option -and changing «new card in random order» to «new cards in order added» -and reciprocally. This action ensure that each card of the deck get a -new `due` value, almost according to the description of the option -(but not totally). - - -## Properties that the `due` value should hold -### Siblings -For the sake of the simplicity, let us assume in this section -(Siblings) that sibling cards belong to the same deck. - -Ideally, if you have seen a new card, you'll see its sibling very -soon. In the best case, as soon as you see a new card from the same -deck, you'll see the new siblings of any card you have already seen, -unless those cards are already buried/suspended. You'll discover a new -note only if no such new siblings exists. This certainly may help to -remember the whole content of the note. - -This is ensured by giving the same `due` date to all siblings. So all -new card with the smallest `due` date are siblings. However when you -reorder a deck, and set it to random order, a new card may have a high -`due` value and siblings which are not new. This means that you'll -have to wait quite some time to see a new sibling of a seen card. - - -The property that all siblings have the same `due` date is true while -notes are created. If each sibling of a card belong to the same deck -(not considering subdecks), and this deck is reordered (i.e. in deck's -option, the new card in random order/order added) is toggled. - -However, this property may get lost in at least two cases: -* if a card is reordered while its sibling is not -* if siblings belong to different deck, and one deck gets reordered - -### Uniqueness -Normally, each card with the same `due` value belongs to the same -note. This is ensured because each time a `due` value is required, -(i.e. when adding a card or reordering a deck), the `due` value -chosen are greater than the maximal `due` value already present in -the collection. - -This property is true almost all the time, at least unless you or an -add-on change the database directly. However, there is one case where -this became false, if the `due` value of a card is 1,000,000 or -greater. In this case: -* until april 2019 (anki 2.1.12): all cards have `due` value 1,000,000. -* since april 2019 (anki 2.1.13): the due values are computed modulo - 1,000,000, which means that you may have a lot of cards with due - value 0, 1, ... - -### Order of adding cards -If a deck has «new card in order added», then the `due` card -represents the order in which card are added to the collection. When -cards are created using anki directly, or imported from a CSV file, it -simply means that the `due` are in the same order than the `created` -value. However, if a deck is imported (for example by downloading it -from ankiweb first), then the `created` value represents the day where -the card was created originally, and not the day were the cards were -added to our collection. - -In particular, it means that the «order added» can never be recomputed -once it is lost. This can be seen using the following steps: -* take a deck with «new card in random order». -* add it a new note -* import a deck (which was created before your new note was created) -* toggle the deck to new card in order added. - -You'll see that the imported notes are shown before the note you -created, while they were added after it. - -### Random order -If a deck has «new cards in random order», the notes should be seen in -an unpredictable order. - -Actually, this occurs only in two cases: -* when the notes are imported in the deck from an anki2 file, -* for cards present in the deck when the deck has been reordered, - -## How to reproduce bugs -In this section, I explain how to have a few strange result, without -using any add-on. The argument here is not that those bug occurs -often. It just illustrate that something may be quite wrong when -things as simple as changing the deck of a card occurs. - -### Random deck not actually random -Create an empty deck, set it to «new cards in random order». Then add -10 notes. Review this deck. You'll see card in order added. The -problem here is that cards created are not randomized, only card -present in the deck when the deck option was changed. - -### How to obtain an arbitrary order in any deck -Create 10 decks, with 10 decks option. In each deck, put a card. Then -reorder each deck. Finally, put the 10 cards in the same deck. If you -review this deck, you'll see the card in the order in which you have -reordered deck. This is far from being the «order added» nor a «random -order». - -### Creating cards of an already existing note -A card may be created for a note already existing. This may occur in -at least two ways. By adding text in an empty field, or by editing the -note type (either changing a card type already existing, or adding a -new card type). - -This may have two distinct effect, depending on whether this note -still have a new card. If a note has a new card, the `due` value of the -created new card is the same as the one of the new card(s) already -existing. However, if no such new card existed, then the `due` value of -the created card is higher than all other `due` value in the collection. - -Both action have sens, depending on whether you want to consider when -notes are added, or when cards are added. But the fact that the `due` -value is so different depending on this property is clearly not what -is expected. - -### `Due` 1,000,000 -As explained above, the `due` of a card is a 32 bits integer. To -ensure that it remains so, when database is checked, every `due` -greater than 1,000,00 is: -* until april 2019 (anki 2.1.12): changed to 1,000,000 . This mean - that as soon as a single card has value 1,000,000, all new cards - have this `due` value. Thus `due` becomes totally useless. -* since april 2019 (anki 2.1.13): changed to their value modulo - 1,000,000. Thus the last card added will have due value 0, 1, - ... and will become the first card to be reviewed. - -According to -(lovac42)[https://anki.tenderapp.com/discussions/ankidesktop/33664-due-value-of-new-card-being-1000000#comment_47198513], -at least 3 of the 10 random deck he checked has `due` value greater than -1,000,000. This means that, each time a user download such a deck, -there is a high risk that this very problem occurs. Worst, if this -user share a deck later, this bug will propagate. - - - -## Possible solution -### Order computed -The first thing which, I believe, should be done is to stop calling -«order added» and use instead «order created». This is quite -important, because that the only think which can actually be computed -by anki. - -### No `due` for new cards except for random order -When you want to select a card in a deck and you want to show cards in -order created, then you can use a sql query which takes new card with -minimal `(id,ord)`. This is as easy as finding minimal `due`, and it -will always work. - -### Select siblings for random decks -In a deck in which new cards are selected in random order, select new -cards with have a non-new sibling first. This can be done by having a -binary flag stating whether there is a non-new sibling. This is non -trivial to code, because this flag may have to change often, but it's -not computationally hard. - -### Recompute `due` in decks more often -As explained above, as soon as new card are added in a random deck, -and as soon as as card move to another deck, the `due` value may become -wrong. Thus I believe that, if `due` is kept, it should be recomputed -when the deck is changed. No need to recompute it each time a card is -added. Just mark the deck as needed recomputation, and do the -recomputation when new cards are reviewed. Note that, this partially -defeat the purpose of preprocessing. - -Note also that this is what does the add-on [https://github.com/Arthur-Milchior/anki-correct-`due`] - -### Do a recomputation of all due values -If database check realize that some `due` value is 1,000,000, then -all due value should be recomputed. As long as there are less than -1,000,000 new card, the order may be kept, while keeping small values. - -As explained above, I highly doubt that keeping the current `due` -order is important, because they have almost no meaning -currently. However, I should note that, this can't simply be -implemented using `col.sched.sortCards` as proposed by -(lovac42)[https://github.com/Arthur-Milchior/anki-correct-due/issues/1]. Indeed, -this method gives the same `due` value to each card of a note, -while different card may have different `due` values, as explained -above. It means that this method may change the order of some new cards. diff --git a/documentation/glossary.md b/documentation/glossary.md deleted file mode 100644 index 63dcf2b7014..00000000000 --- a/documentation/glossary.md +++ /dev/null @@ -1,37 +0,0 @@ -Here is the documentation of vocabulary found in anki code and not in -anki's documentation. Because it's mostly internal stuff. - -# Collection -## Deck conf -That's one of the option which can be applied to a deck in the main -page. - -## Model -What code calls model is called «note type» in Anki's -documentation. The name model is still found in the synchronization -protocol, and in «mid», which represents id of note type. - -## Template -What code calls template is called «card type» in Anki's -documentation. - -## id's -The idea of an object is often denoted as `Oid`, with O the -initial of the kind of object. Thus cid, nid, mid, did and oid, for -ids of card, note, model, deck, and original deck. - -However, in the object itself, the id is denoted as `id` and not -as `Oid`. For example, the id of a card is `card.id`. - - -# Main window -The main window may display different kind of content. The «state» of -the main window is the kind of content on it. - -* Reviewer: The window with a card, and button to answer. -* DeckBrowser: The list of decks -* sync: waiting for sync to finish (the content is the same as - deckbrowser) -* overview: what you obtain when selecting a deck but not yet studying it -* profileManager: no window, instead ask to choose for a profile -* startup: empty window, before collection is loaded. diff --git a/documentation/hooks.md b/documentation/hooks.md deleted file mode 100644 index 8042fef62d1..00000000000 --- a/documentation/hooks.md +++ /dev/null @@ -1,442 +0,0 @@ -# All hooks in anki. -This document contains a description of all hooks in Anki at time of -writting. - -Each description contains the argument the hook expect. A description -of when the hook is called. And which functions are in the -hook. Either in anki's code. Or in some add-ons, which may serve as -example. - -## Hooks -Some hooks are similar. For examples, hooks used to add action to -context menu (menu opened by right clicking). Thus, I sort the hooks -by similarity. - -### Menues -In this section, we consider menues. Either obtained by doing a right -click. Or by the top-bar menu. - -Unless specified otherwise, no functions are added to the hook. - -#### Top menu -##### browser.setupMenus -Called from browser when menus are created. - -#### Context menues -A context menu is obtained by right-clicking somewhere. - -Unless specified otherwise, those hooks takes two arguments: -* self: the window or subwindow on which a right click is done -* m: a qmenu object which will be shown near to the click. In which - more actions can be added. - -##### AnkiWebView.contextMenuEvent -It allows to add actions to do by right clicking in any html window -using aqt.webview. - -Called in aqt.webview.AnkiWebView.contextMenuEvent - -##### browser.onContextMenu -Called when right clicking in the columns of the browser. - -Called in aqt.browser.Browser.onContextMenu. - -##### EditorWebView.contextMenuEvent -Called from aqt.editor.EditorWebView.contextMenuEvent, the part of the -window used to edit note. - -##### Reviewer.contextMenuEvent -Called from aqt.reviewer.Reviewer.showContextMenu: the class for the -reviewer of cards in the main window - -#### Other menues -###### setupEditorButtons -Called from aqt.editor.Editor.setupWeb, with the buttons on right top, -and the editor itself. It would allow to add more buttons on the top -right of editors - -##### mungeEditingFontName -Contains aqt.editor.fontMungeHack, which return the input, by -replacing the end of the string, if it's " L" by " Light". Because of -some difference between name in QFont and WebEngine. - -Called in aqt.editor.Editor.fonts, when the list of filters is generated. - -##### showDeckOptions -Called from aqt.deckbrowser.DeckBrowser._showOptions. Allow to add -actions to do to list of decks, after Rename, options, export, and -delete. - -For example, add-on [Change option -recursively](https://ankiweb.net/shared/info/751420631) uses it to add -the way to change option of deck and subdecks. - -##### AddCards.onHistory -Called from aqt.addcards.AddCards.onHistory, i.e. when you click on -the History button of the "add card" window. It allow mostly to add -more cards, for example in addon [Open 'Added Today' from -Reviewer](https://ankiweb.net/shared/info/861864770). - -### State changes -The hooks in this section are called when something change. - -##### exportersList -Arguments is: -* exps: a list of exporter (i.e. class implementing - anki.exporting.Exporter. ) - -It is called in anki.exporting.exporters(), a function which returns -the list of exporters existing in the code. It allows to add (or -remove ?) exporters, and thus to export in other file format. Thus it -allows to add other element in the export window. - -#### Model Change -##### odueInvalid -Contains anki.main.AnkiQt.onOdueInvalid, which show a warning and ask -to check the database. - -Called from anki.cards.Cards.flush and .flushSched if a card's queue -is rev, has an odue value but is not in a dynamic deck. - -##### afterStateChange -##### beforeStateChange -It's called just after and just before the main window's state -changed. - -Arguments are: state, oldState, *args - -See aqt.main's documentation to know which are the potential states. - -##### currentModelChanged -Called when the note type (a.k.a. model) is changed. It's run only -from modelchooser. It the windows importdialog, deckchooser, -changemodel and addcards are opened/closed, the following methods are -added/removed from this hook: aqt.importing.ImportDialog.modelChanged, -aqt.deckchooser.DeckChooser.onModelChange, -aqt.browser.ChangeModel.onReset and -aqt.addcards.AddCards.onModelChange. - -##### reset -Contains the method self.onReset, where self is the class -aqt.addcards.AddCards, aqt.modelchooser.ModelChooser, -aqt.studydeck.StudyDeck, aqt.browser.Browser, aqt.browser.ChangeModel, -aqt.main.AnkiQt, aqt.editcurrent.EditCurrent, while those windows are -opened. This function is essentially similar to reopening the window, -setting everything according to the current state of the collection. - -It is called from aqt.AnkiQt.reset, i.e. when the collection should be -reseted, if a collection is loaded. This is called often, when -anything occurs to the collection. - -##### leech -Contains anki.reviewer.onLeech. Which state that a card is a leech, -and maybe that it is suspended. It also contains function for tests -(not considered in this documentation). - -It is called from both scheduler, when a card become a leech. - -##### remNotes -Contains aqt.main.AnkiQt.onRemNotes, a method adding a line about -deletion in the file deleted.txt. The argument is the collection and -the ids of the removed notes. - -It is called from anki.collection._Collection._remNotes. - -##### newDeck -Called from anki.decks.DeckManager.id when a new deck is created. - -Contains aqt.browser.Browser.maybeRefreshSidebar (if the browser is -on), which change the side bar of the browser if it is visible, to -show the new deck. - -##### newModel -Called from anki.ModelManager.models.save each time note type -(a.k.a. models) are saved (even if the model is not new) - -Contains aqt.browser.Browser.maybeRefreshSidebar (if the browser is -on), which change the side bar of the browser if it is visible, to -show the new deck. - -##### modSchema -Contains aqt.main.AnkiQt.onSchemaMod, which ask the used to confirm -they accept a change requiring to upload the full database. - -Is called as a filter in anki.collection._Collection.modSchema, with -parameter True. If it returns False, i.e. the user cancels, then an -error is raised, aborting the schema modification. - -##### newTag -Called from anki.tags.TagManager.register(tags) if at least one of the -tag of tags is new. - -Contains aqt.browser.Browser.maybeRefreshSidebar (if the browser is -on), which change the side bar of the browser if it is visible, to -show the new deck. - -#### GUI change -###### editFocusLost -contains aqt.browser.Browser.refreshCurrentCardFilter, -i.e. aqt.browser.Browser.refreshCurrentCard. Hence refresh the note in -the dataModel and (re)render the preview. - -Called from aqt.editor.Editor.onBridgeCmd when the command starts by -"Blur" (I have no idea what it means) - -##### setupEditorShortcuts -Arguments are: cuts, self - -Called from aqt.editor.Editor.setupShortcuts, i.e. when the note -editor is shown somewhere. - -The two arguments are the list of shortcuts, and the editor. This -allow to add more shortcut to the editor. - -##### self.state+"StateShortcuts" -Called from aqt.main.AnkiQt.setStateShortcuts. I.e. for -aqt.reviewer.Reviewer.show and aqt.overview.Overview.show. - -The argument is the list of shortcuts for the current states. It -allows to add more shortcut to this state. - -###### setupStyle -Called from aqt.main.AnkiQt.setupStyle. Hence during -aqt.main.AnkiQt.setupUI. Takes as argument the buffer, as a string,, -with its QMenuBar and QTleeWidget. Not clear exactly what it does, -because I don't know graphical user interface. - -##### profileLoaded -Called in anki.main.AnkiQt.loadProfile, after the end of the loading, -before calling the onsuccess function given in argument. Note that the -collection is not yet accessible from the main window. - -An add-on used this to ensure that some event is executed after -profile is loaded, and not at start-up as is the default. - -##### unloadProfile -Called anki.main.AnkiQt.unloadProfile, i.e. when anki exits, when -there is a change of profile. It contains anki.sound.stopMplayer, -which stop the music player. - -##### tagsUpdated -Called from aqt.editor.Editor.saveTags, with as argument the note -contained in the editor. Hence when the note should be saved. - -##### browser.rowChanged -Called when the selected card(s) are changed in the browser. After the -change is processed, but before rendering the view. - -##### loadNote -It's called from aqt.editor.Editor.loadNote.oncallback, when a note is -loaded in the editor (i.e. the bottom part appearing in the browser -when a single card is selected). - -Contains aqt.browser.Browser.onLoadNote while the browser is -open. Takes as argument the editor. This function refresh the current -card, using the note in the editor. - -##### undoState: -Contains aqt.browser.Browser.onUndoState(on) if a browser is -opened. This method turn the undo action on or off according to the -Boolean value of `on`. It change the text of this action. - -It is called from aqt.main.AnkiQt.maybeEnableUndo, which change the -undo action from the main window. - -##### showAnswer -Called at the end of `aqt.reviewer.Reviewer._showAnswer`. - -##### showQuestion -Called at the end of `aqt.reviewer.Reviewer._showQuestion`. - -### Unknown -Those hooks are called in a part of code I don't understand yet. Help -is welcomed. - -##### mpvWillPlay -Contains aqt.main.AnkiQt.onMpvWillPlay, which seems to change the -_activeWindowOnPlay to some window, unless the argument file is a video. - -Called from anki.sound.MpvManager.queueFile(file), with the file as -argument. This method is called while anki.sound.setupMpv, from -anki.main.AnkiQt.setupSound, from anki.main.AnkiQt.setupUI, from -anki.main.AnkiQt.__init__. Hence it seems to be called only once. - -##### editFocusGained -Arguments are: self.note, self.currentField - -##### editTimer -Arguments are: self.note - -Contains aqt.browser.Browser.refreshCurrentCard when the browser is open. -##### exportedMediaFiles -Arguments are: c: a path - -Contains aqt.exporting.ExportDialog.exportedMedia but only while -aqt.exporting.ExportDialog.accept runs, while it calls -aqt.exporting.ExportDialog.exporter.exportInto where -aqt.exporting.ExportDialog.exporter is some class of anki.exporting. - -Called from anki.AnkiPackageExporter._exportMedia each time a media is -exported. - -##### noteChanged -Called from anki.main.AnkiQt.noteChanged(nid), with argument -nid. Seems to never be called. - -### Network -##### httpRecv - -Contains a function recvEvent while some download occur. The hook is -removed when the download ended. - -The download may be either an add-on, or a synchronization. - -This method takes as argument a number of byt, add it to some count -`self.recvTotal` and then emitting something as a QT object. In -the case of synchronization, if the sync is aborted, it raises an -exception. - -It is called from anki.sync.AnkiRequestsClient.streamcontent when a -chink of data is received. This data may be either an add-on or a synchronization. - -##### httpSend -Similar to httpRecv, but for emissiond of synchronization, and not for -reception. - -Called from anki.sync._MonitoringFile.read. - -##### sync -Contains, equivalently: -* (lambda type:aqt.sync.SyncThread.fireEvent("sync",type)), -* (lambda type:aqt.sync.SyncThread.event.emit("sync",type)) -* (lambda type:aqt.sync.onEvent("sync",type)) -but only while aqt.sync.SyncThread.run runs, i.e. while anki synchronization is -running. Here, event is a pyqtSignal(str, str) - -It is called from anki.sync.Syncer.sync, each time the -synchronization's step change. The different step/type are, in this -order: -* login: just after saving the collection -* meta: startup and deletions -* server: stream large tables from server -* client: stream to server -* sanity: sanity check -* finalize - -Furthermore, there is the step/type "stream" called while streaming -occur in steps Server and client. - -It's not clear what is the goal of this, since the output of the hook -is never used, and the function does not have any access to the -synchronizer object. - -It's also called from anki.sync.FullSyncer.download, with the -following type: -* download -* upgradeRequired -and from anki.sync.FullSyncer.uplad, with the types: -* upload - -And from anki.sync.MediaSyncer.sync, with: -* findMedia. - -##### syncMsg -Contains aqt.sync.SyncThread.run.syncMsg, hence (lambda -msg:aqt.sync.SyncThread.fireEvent("syncMsg",msg)), hence (lambda -msg:aqt.sync.SyncThread.event.emit("syncMsg",msg)), where event is a -pyqtSignal(str, str). - -It contains this function only while the synchronization thread -runs. - -It's called from anki.sync.MediaSyncer.sync with a text stating how -many media remains to upload. And from -anki.sync.MediaSyncer._downloadFiles, where it states how many media -remains to download. - - -### Adding options -##### search -called from anki.find.Finder.__init__, with the argument -anki.find.Finder.search. This is a dictionnary from terms which can be -used in a search (in browser, in filtered deck) to a function giving a -sql query satisfying this search. This allow to add new terms and -function to this list. - -### fmod -The following four filters are called from -anki.template.template.Template.render_unescaped. When a field start -by `{{foo:`, i.e. it is `{{foo:bar}}`, the filter fmod_foo is -called on the content of the field bar. This may occur multiple time, -since multiple modifier may begin a mustache. - -For example, the add-on [Edit Field During -Review](https://ankiweb.net/shared/info/1549412677) add a hook -fmod_edit, so that you can use `{{edit:` in card type. - -##### fmod_kanji -##### fmod_kana -##### fmod_furigana -Contains methods anki.template.kanji, .kana, and .furigana. This edit -the text given in argument. Replacing   by spaces. And then some -transformation which I don't understand. It's supposed to be for Japan language. - -##### fmod_hint -Contains anki.template.hint.hint. A method which generate html for -hints link in questions. And the js code which replace the hint -link by the hint. - - - -#### Hooks called, but from methods which are never called themselves. -##### colLoading -Arguments are: self.col -It seems to be never called. More precisely, it's called in -aqt.main.AnkiQt._colLoadingState, which itself is never called. -##### mpvIdleHook -Contains aqt.main.AnkiQt.onMpvIdle. Not clear yet what it does. - -Called from anki.sound.MpvManager.on_idle, which seems to be never -called. - -##### reviewCleanup -Called from aqt.reviewer.Reviewer.cleanup, hence from -anki.main.AnkiQt._reviewCleanup assuming the new state is neither -"resetRequired" nor "review". Which itself seems to never be called. - - - -### Preparing question and answers -##### mungeQA -Contains anki.latex.mungeQA. A method which generate HTML which show -an image which contains the compilation result of the LaTeX given in input. - -Called from anki.collection._Collection._renderQA - -###### prepareQA -Called from aqt.reviewer.Reviewer._showQuestion, with the text of the -question, the card object and the text "reviewQuestion". - -Called from aqt.reviewer.Reviewer._showAnswer, with the text of the -answer, the card object and the text "reviewAnswer". - -Called from aqt.clayout.Reviewer._renderPreview, with the text of the -question, the card object and the text "reviewQuestion". - -Called from aqt.clayout.Reviewer._renderPreview, with the text of the -answer, the card object and the text "reviewAnswer". - -###### mungeFields -Called from aqt.collection._Collection._renderQA, after {{type: are -processed, wrapping a template in `{{^Field}}` will not do what you expect. - -But it let us guessing which are the cases creating trouble and which -cases are ok. - -This document explains which are the rules which decides when a card -is generated or not. Those rules are implemented in -[models.py](../anki/models.py), in the method `_reqForTemplate`. - -I assume that those rules are created in order to make anki quicker, -and less expensive in computation time. Alas, it makes anki more -complicated for power user. - -## The rules - -### Non cloze models - -Anki generate the html content of some cards in some cases. It checks -this content to choose what kind of rules should be applied - -First, anki generates the html of card where all fields are -empty. We'll call this content `empty content`. We'll use this -twice below. - - -#### Everything and Nothing -Anki generates the content where every fields contains "ankiflag". It -then checks whether the result is equal to `empty content`. -Intuitively, if you have the same result when everything is filled and -when everything is empty, it probably means that the template does not -consider its input. Anki then consider that this template can be -discarded. - -In this case, the method `_reqForTemplate` returns `("none",[],[])`. - -This is why the documentation state that you should not put everything -inside `{{^Field}}...{{/Field}}`. If you do that, then each time -the field `Field` is filled, the html content of this card is -empty. And thus anki believes that the note never generate any -content. - -In particular it means that no card with this note type will ever be -generated. - -### Removing one field -Now we know that some fields are actually used in the template, -and that, if every fields are filled, we have some content. Now, we -consider what happens if a single field is missing. - -Thus, we generate the html content, when a field `Field` is empty, -and every other field contains the text "ankiflag". If we also find -"ankiflag" in the result, it means a field was shown, thus `Field` -is not mandatory. - -If we don't find "ankiflag" in the result, we consider `Field` to -be mandatory. - -If there is at least one mandatory field, `_reqForTemplate` -returns the pair `("all",l)` where `l` is the list of -mandatory flags. - -In this case, cards are generated for this template if and only if all -of those "mandatory" fields are filled. - -### Using a single field -We now assume that no fields are mandatory. In this case we check for -fields which are sufficient by themselves to generate a card with some -content. - - -Thus, we generate the content, when a field `Field` contains "1", -and every other fields is empty. If the result is not the same as the -html `empty content` computed above, then we consider that the -field `Field` is sufficient to generate the card. - -Then `_reqForTemplate` returns `("any",l)` where `l` is -the list of sufficient field. Note that the list may be empty. - -In this case, cards are generated for this template if and only if all -of those "mandatory" fields are filled. From 9c5f2e939b0dfd66d8aebdddb85d186d2b7ae8d3 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Sat, 15 Jun 2019 15:42:29 +0200 Subject: [PATCH 12/68] documenting deletion process --- documentation/deletion.md | 67 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 documentation/deletion.md diff --git a/documentation/deletion.md b/documentation/deletion.md new file mode 100644 index 00000000000..0eee896616f --- /dev/null +++ b/documentation/deletion.md @@ -0,0 +1,67 @@ +# Deletion +Deleting cards and notes seems to be a simple notion. Actually, there +are plenty of cases which are not straightforward and which needs to +be taken into account. + +## Deleting a note directly + +This is the simplest case. If you select a card in the browser, or is +reviewing a card, and you decide to delete it, you'll delete the +entire note. This is also what occurs if you delete a note type. (Note +that if you have in your collection a note whose note type is unkwown, +or with a wrong number of field, or which has no card, it will also be +silently deleted during a database check.) + +## Deleting cards +Here are the following way to delete a card. +* deleting a deck +* deletion requested by a synchronization. + +It should be noted that, in those two cases, if the note still +exists and the card is not empty, the next «check database» will +regenerate those cards, but they'll be considered to be brand new +cards. + +* changing a note's note type. + +You change use «change the note type» without actually changing it, +and just moving a card to «nothing». Note however that, if this card +is not empty, it will immediatly be regenerated. Thus what you are +doing is only changing this card for a new card (and potentially +changing it's deck, because anki may have to guess the deck for the +new card.) + +* deleting a card type (this can not be done if this is the card type + of the only card of a note) +* deleting the note containing the card +* deleting empty cards + +Of course this last option only delete empty cards. + +* doing a database check +this delete cards if the card has a position which is incorrect, or if +it's note does not exists anymore. + + +## Deleting each card of a note +By default, if each card of a note are deleted, then the note is also +deleted. + +There is one exception of this rule: when the card suppresion is due +to a synchronization. In this case, the note may stay in your +collection, with no card. I believe that the reason for this choice is +that some cards may be downloaded later in the synchronization for +this note, thus this note should be keep. And anyway, if there are +really no card at all, then the note should also be deleted in the server, +and thus the synchronization will eventually request the deletion of +the note. (See [synchronization.md] for more details) + + + +This rules sometimes make sens. If you delete a deck, you probably don't +expect it's card to respawn later in another deck as new card. Except +that it also means that if you did a mistake in a note template, you +may lose every note when you click on «Empty cards...». And while it +may be acceptable to lose cards (especially if they are all new), I +believe that losing a note is a huge problem, because you can't +recreate them automatically. From bae13d69cd0f50e798e1c9d92a4fb16f5b2b9d57 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Sat, 15 Jun 2019 15:42:42 +0200 Subject: [PATCH 13/68] documenting due --- documentation/due.md | 214 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 documentation/due.md diff --git a/documentation/due.md b/documentation/due.md new file mode 100644 index 00000000000..3062d27f968 --- /dev/null +++ b/documentation/due.md @@ -0,0 +1,214 @@ +This document discuss the `due` value of new card. It's main goal +is to express why I believe it to be currently broken. I will give +plenty of way to have wrong `due` values, without even installing an +add-on. + +It also propose ways to fix it, and explain the problem I see with all +of them. Which, I believe, argues that the definition of `due` should be +slightly changed. + +In this document, I will only consider new cards, but I will not +repeat it again. + +# Definition +Because some terms are confusing, I have to state some explicit +definition now. + +## New card. +When I write «new card» in this document, it always mean a card which +has the property to be «new». It may be a card created years ago. When +I speak of a card which has just been created, I'll write «the created +card». + +## What is `due` ? +New cards are shown in a particular order. Computing this order may +takes some time. So, instead, anki pre-compute this order. This +precomputation is called `due`. Each new card has an integer value +called `due`. The cards which should be seen first have the lowest +`due` value. + +The `due` value is a 32 bit integer. This is ensured because each time +a `due` value is greater than 1,000,000, it is changed to 1,000,000 +during database check. + +## Reordering a deck +I'll speak a lot about "reordering a deck", so let me introduce this +notion here. By «reordering a deck», I mean going to the deck option +and changing «new card in random order» to «new cards in order added» +and reciprocally. This action ensure that each card of the deck get a +new `due` value, almost according to the description of the option +(but not totally). + + +## Properties that the `due` value should hold +### Siblings +For the sake of the simplicity, let us assume in this section +(Siblings) that sibling cards belong to the same deck. + +Ideally, if you have seen a new card, you'll see its sibling very +soon. In the best case, as soon as you see a new card from the same +deck, you'll see the new siblings of any card you have already seen, +unless those cards are already buried/suspended. You'll discover a new +note only if no such new siblings exists. This certainly may help to +remember the whole content of the note. + +This is ensured by giving the same `due` date to all siblings. So all +new card with the smallest `due` date are siblings. However when you +reorder a deck, and set it to random order, a new card may have a high +`due` value and siblings which are not new. This means that you'll +have to wait quite some time to see a new sibling of a seen card. + + +The property that all siblings have the same `due` date is true while +notes are created. If each sibling of a card belong to the same deck +(not considering subdecks), and this deck is reordered (i.e. in deck's +option, the new card in random order/order added) is toggled. + +However, this property may get lost in at least two cases: +* if a card is reordered while its sibling is not +* if siblings belong to different deck, and one deck gets reordered + +### Uniqueness +Normally, each card with the same `due` value belongs to the same +note. This is ensured because each time a `due` value is required, +(i.e. when adding a card or reordering a deck), the `due` value +chosen are greater than the maximal `due` value already present in +the collection. + +This property is true almost all the time, at least unless you or an +add-on change the database directly. However, there is one case where +this became false, if the `due` value of a card is 1,000,000 or +greater. In this case: +* until april 2019 (anki 2.1.12): all cards have `due` value 1,000,000. +* since april 2019 (anki 2.1.13): the due values are computed modulo + 1,000,000, which means that you may have a lot of cards with due + value 0, 1, ... + +### Order of adding cards +If a deck has «new card in order added», then the `due` card +represents the order in which card are added to the collection. When +cards are created using anki directly, or imported from a CSV file, it +simply means that the `due` are in the same order than the `created` +value. However, if a deck is imported (for example by downloading it +from ankiweb first), then the `created` value represents the day where +the card was created originally, and not the day were the cards were +added to our collection. + +In particular, it means that the «order added» can never be recomputed +once it is lost. This can be seen using the following steps: +* take a deck with «new card in random order». +* add it a new note +* import a deck (which was created before your new note was created) +* toggle the deck to new card in order added. + +You'll see that the imported notes are shown before the note you +created, while they were added after it. + +### Random order +If a deck has «new cards in random order», the notes should be seen in +an unpredictable order. + +Actually, this occurs only in two cases: +* when the notes are imported in the deck from an anki2 file, +* for cards present in the deck when the deck has been reordered, + +## How to reproduce bugs +In this section, I explain how to have a few strange result, without +using any add-on. The argument here is not that those bug occurs +often. It just illustrate that something may be quite wrong when +things as simple as changing the deck of a card occurs. + +### Random deck not actually random +Create an empty deck, set it to «new cards in random order». Then add +10 notes. Review this deck. You'll see card in order added. The +problem here is that cards created are not randomized, only card +present in the deck when the deck option was changed. + +### How to obtain an arbitrary order in any deck +Create 10 decks, with 10 decks option. In each deck, put a card. Then +reorder each deck. Finally, put the 10 cards in the same deck. If you +review this deck, you'll see the card in the order in which you have +reordered deck. This is far from being the «order added» nor a «random +order». + +### Creating cards of an already existing note +A card may be created for a note already existing. This may occur in +at least two ways. By adding text in an empty field, or by editing the +note type (either changing a card type already existing, or adding a +new card type). + +This may have two distinct effect, depending on whether this note +still have a new card. If a note has a new card, the `due` value of the +created new card is the same as the one of the new card(s) already +existing. However, if no such new card existed, then the `due` value of +the created card is higher than all other `due` value in the collection. + +Both action have sens, depending on whether you want to consider when +notes are added, or when cards are added. But the fact that the `due` +value is so different depending on this property is clearly not what +is expected. + +### `Due` 1,000,000 +As explained above, the `due` of a card is a 32 bits integer. To +ensure that it remains so, when database is checked, every `due` +greater than 1,000,00 is: +* until april 2019 (anki 2.1.12): changed to 1,000,000 . This mean + that as soon as a single card has value 1,000,000, all new cards + have this `due` value. Thus `due` becomes totally useless. +* since april 2019 (anki 2.1.13): changed to their value modulo + 1,000,000. Thus the last card added will have due value 0, 1, + ... and will become the first card to be reviewed. + +According to +(lovac42)[https://anki.tenderapp.com/discussions/ankidesktop/33664-due-value-of-new-card-being-1000000#comment_47198513], +at least 3 of the 10 random deck he checked has `due` value greater than +1,000,000. This means that, each time a user download such a deck, +there is a high risk that this very problem occurs. Worst, if this +user share a deck later, this bug will propagate. + + + +## Possible solution +### Order computed +The first thing which, I believe, should be done is to stop calling +«order added» and use instead «order created». This is quite +important, because that the only think which can actually be computed +by anki. + +### No `due` for new cards except for random order +When you want to select a card in a deck and you want to show cards in +order created, then you can use a sql query which takes new card with +minimal `(id,ord)`. This is as easy as finding minimal `due`, and it +will always work. + +### Select siblings for random decks +In a deck in which new cards are selected in random order, select new +cards with have a non-new sibling first. This can be done by having a +binary flag stating whether there is a non-new sibling. This is non +trivial to code, because this flag may have to change often, but it's +not computationally hard. + +### Recompute `due` in decks more often +As explained above, as soon as new card are added in a random deck, +and as soon as as card move to another deck, the `due` value may become +wrong. Thus I believe that, if `due` is kept, it should be recomputed +when the deck is changed. No need to recompute it each time a card is +added. Just mark the deck as needed recomputation, and do the +recomputation when new cards are reviewed. Note that, this partially +defeat the purpose of preprocessing. + +Note also that this is what does the add-on [https://github.com/Arthur-Milchior/anki-correct-`due`] + +### Do a recomputation of all due values +If database check realize that some `due` value is 1,000,000, then +all due value should be recomputed. As long as there are less than +1,000,000 new card, the order may be kept, while keeping small values. + +As explained above, I highly doubt that keeping the current `due` +order is important, because they have almost no meaning +currently. However, I should note that, this can't simply be +implemented using `col.sched.sortCards` as proposed by +(lovac42)[https://github.com/Arthur-Milchior/anki-correct-due/issues/1]. Indeed, +this method gives the same `due` value to each card of a note, +while different card may have different `due` values, as explained +above. It means that this method may change the order of some new cards. From 78b0237e9cfb7bd63c00ef60b69fe52fa36e8645 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Sat, 15 Jun 2019 15:43:06 +0200 Subject: [PATCH 14/68] listing and documenting hooks --- documentation/hooks.md | 450 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 450 insertions(+) create mode 100644 documentation/hooks.md diff --git a/documentation/hooks.md b/documentation/hooks.md new file mode 100644 index 00000000000..aff3b8ec719 --- /dev/null +++ b/documentation/hooks.md @@ -0,0 +1,450 @@ +# All hooks in anki. +This document contains a description of all hooks in Anki at time of +writting. + +Each description contains the argument the hook expect. A description +of when the hook is called. And which functions are in the +hook. Either in anki's code. Or in some add-ons, which may serve as +example. + +## Hooks +Some hooks are similar. For examples, hooks used to add action to +context menu (menu opened by right clicking). Thus, I sort the hooks +by similarity. + +### Menues +In this section, we consider menues. Either obtained by doing a right +click. Or by the top-bar menu. + +Unless specified otherwise, no functions are added to the hook. + +#### Top menu +##### browser.setupMenus +Called from browser when menus are created. + +#### Context menues +A context menu is obtained by right-clicking somewhere. + +Unless specified otherwise, those hooks takes two arguments: +* self: the window or subwindow on which a right click is done +* m: a qmenu object which will be shown near to the click. In which + more actions can be added. + +##### AnkiWebView.contextMenuEvent +It allows to add actions to do by right clicking in any html window +using aqt.webview. + +Called in aqt.webview.AnkiWebView.contextMenuEvent + +##### browser.onContextMenu +Called when right clicking in the columns of the browser. + +Called in aqt.browser.Browser.onContextMenu. + +##### EditorWebView.contextMenuEvent +Called from aqt.editor.EditorWebView.contextMenuEvent, the part of the +window used to edit note. + +##### Reviewer.contextMenuEvent +Called from aqt.reviewer.Reviewer.showContextMenu: the class for the +reviewer of cards in the main window + +#### Other menues +###### setupEditorButtons +Called from aqt.editor.Editor.setupWeb, with the buttons on right top, +and the editor itself. It would allow to add more buttons on the top +right of editors + +##### mungeEditingFontName +Contains aqt.editor.fontMungeHack, which return the input, by +replacing the end of the string, if it's " L" by " Light". Because of +some difference between name in QFont and WebEngine. + +Called in aqt.editor.Editor.fonts, when the list of filters is generated. + +##### showDeckOptions +Called from aqt.deckbrowser.DeckBrowser._showOptions. Allow to add +actions to do to list of decks, after Rename, options, export, and +delete. + +For example, add-on [Change option +recursively](https://ankiweb.net/shared/info/751420631) uses it to add +the way to change option of deck and subdecks. + +##### AddCards.onHistory +Called from aqt.addcards.AddCards.onHistory, i.e. when you click on +the History button of the "add card" window. It allow mostly to add +more cards, for example in addon [Open 'Added Today' from +Reviewer](https://ankiweb.net/shared/info/861864770). + +### State changes +The hooks in this section are called when something change. + +##### exportersList +Arguments is: +* exps: a list of exporter (i.e. class implementing + anki.exporting.Exporter. ) + +It is called in anki.exporting.exporters(), a function which returns +the list of exporters existing in the code. It allows to add (or +remove ?) exporters, and thus to export in other file format. Thus it +allows to add other element in the export window. + +#### Model Change +##### odueInvalid +Contains anki.main.AnkiQt.onOdueInvalid, which show a warning and ask +to check the database. + +Called from anki.cards.Cards.flush and .flushSched if a card's queue +is rev, has an odue value but is not in a dynamic deck. + +##### afterStateChange +##### beforeStateChange +It's called just after and just before the main window's state +changed. + +Arguments are: state, oldState, *args + +See aqt.main's documentation to know which are the potential states. + +##### currentModelChanged +Called when the note type (a.k.a. model) is changed. It's run only +from modelchooser. It the windows importdialog, deckchooser, +changemodel and addcards are opened/closed, the following methods are +added/removed from this hook: aqt.importing.ImportDialog.modelChanged, +aqt.deckchooser.DeckChooser.onModelChange, +aqt.browser.ChangeModel.onReset and +aqt.addcards.AddCards.onModelChange. + +##### reset +Contains the method self.onReset, where self is the class +aqt.addcards.AddCards, aqt.modelchooser.ModelChooser, +aqt.studydeck.StudyDeck, aqt.browser.Browser, aqt.browser.ChangeModel, +aqt.main.AnkiQt, aqt.editcurrent.EditCurrent, while those windows are +opened. This function is essentially similar to reopening the window, +setting everything according to the current state of the collection. + +It is called from aqt.AnkiQt.reset, i.e. when the collection should be +reseted, if a collection is loaded. This is called often, when +anything occurs to the collection. + +##### leech +Contains anki.reviewer.onLeech. Which state that a card is a leech, +and maybe that it is suspended. It also contains function for tests +(not considered in this documentation). + +It is called from both scheduler, when a card become a leech. + +##### remNotes +Contains aqt.main.AnkiQt.onRemNotes, a method adding a line about +deletion in the file deleted.txt. The argument is the collection and +the ids of the removed notes. + +It is called from anki.collection._Collection._remNotes. + +##### newDeck +Called from anki.decks.DeckManager.id when a new deck is created. + +Contains aqt.browser.Browser.maybeRefreshSidebar (if the browser is +on), which change the side bar of the browser if it is visible, to +show the new deck. + +##### newModel +Called from anki.ModelManager.models.save each time note type +(a.k.a. models) are saved (even if the model is not new) + +Contains aqt.browser.Browser.maybeRefreshSidebar (if the browser is +on), which change the side bar of the browser if it is visible, to +show the new deck. + +##### modSchema +Contains aqt.main.AnkiQt.onSchemaMod, which ask the used to confirm +they accept a change requiring to upload the full database. + +Is called as a filter in anki.collection._Collection.modSchema, with +parameter True. If it returns False, i.e. the user cancels, then an +error is raised, aborting the schema modification. + +##### newTag +Called from anki.tags.TagManager.register(tags) if at least one of the +tag of tags is new. + +Contains aqt.browser.Browser.maybeRefreshSidebar (if the browser is +on), which change the side bar of the browser if it is visible, to +show the new deck. + +#### GUI change +###### editFocusLost +contains aqt.browser.Browser.refreshCurrentCardFilter, +i.e. aqt.browser.Browser.refreshCurrentCard. Hence refresh the note in +the dataModel and (re)render the preview. + +Called from aqt.editor.Editor.onBridgeCmd when the command starts by +"Blur" (I have no idea what it means) + +##### setupEditorShortcuts +Arguments are: cuts, self + +Called from aqt.editor.Editor.setupShortcuts, i.e. when the note +editor is shown somewhere. + +The two arguments are the list of shortcuts, and the editor. This +allow to add more shortcut to the editor. + +##### self.state+"StateShortcuts" +Called from aqt.main.AnkiQt.setStateShortcuts. I.e. for +aqt.reviewer.Reviewer.show and aqt.overview.Overview.show. + +The argument is the list of shortcuts for the current states. It +allows to add more shortcut to this state. + +###### setupStyle +Called from aqt.main.AnkiQt.setupStyle. Hence during +aqt.main.AnkiQt.setupUI. Takes as argument the buffer, as a string,, +with its QMenuBar and QTleeWidget. Not clear exactly what it does, +because I don't know graphical user interface. + +##### profileLoaded +Called in anki.main.AnkiQt.loadProfile, after the end of the loading, +before calling the onsuccess function given in argument. Note that the +collection is not yet accessible from the main window. + +An add-on used this to ensure that some event is executed after +profile is loaded, and not at start-up as is the default. + +##### unloadProfile +Called anki.main.AnkiQt.unloadProfile, i.e. when anki exits, when +there is a change of profile. It contains anki.sound.stopMplayer, +which stop the music player. + +##### tagsUpdated +Called from aqt.editor.Editor.saveTags, with as argument the note +contained in the editor. Hence when the note should be saved. + +##### browser.rowChanged +Called when the selected card(s) are changed in the browser. After the +change is processed, but before rendering the view. + +##### loadNote +It's called from aqt.editor.Editor.loadNote.oncallback, when a note is +loaded in the editor (i.e. the bottom part appearing in the browser +when a single card is selected). + +Contains aqt.browser.Browser.onLoadNote while the browser is +open. Takes as argument the editor. This function refresh the current +card, using the note in the editor. + +##### undoState +Contains aqt.browser.Browser.onUndoState(on) if a browser is +opened. This method turn the undo action on or off according to the +Boolean value of `on`. It change the text of this action. + +It is called from aqt.main.AnkiQt.maybeEnableUndo, which change the +undo action from the main window. + +##### revertedCard +Called with the id of a card, when the reviewer is loaded, and the +last review is undone. + +##### revertedState +Similar to undoState, but when «undo» is called from the main +window, not in review mode. + +##### showAnswer +Called at the end of `aqt.reviewer.Reviewer._showAnswer`. + +##### showQuestion +Called at the end of `aqt.reviewer.Reviewer._showQuestion`. + +### Unknown +Those hooks are called in a part of code I don't understand yet. Help +is welcomed. + +##### mpvWillPlay +Contains aqt.main.AnkiQt.onMpvWillPlay, which seems to change the +_activeWindowOnPlay to some window, unless the argument file is a video. + +Called from anki.sound.MpvManager.queueFile(file), with the file as +argument. This method is called while anki.sound.setupMpv, from +anki.main.AnkiQt.setupSound, from anki.main.AnkiQt.setupUI, from +anki.main.AnkiQt.__init__. Hence it seems to be called only once. + +##### editFocusGained +Arguments are: self.note, self.currentField + +##### editTimer +Arguments are: self.note + +Contains aqt.browser.Browser.refreshCurrentCard when the browser is open. +##### exportedMediaFiles +Arguments are: c: a path + +Contains aqt.exporting.ExportDialog.exportedMedia but only while +aqt.exporting.ExportDialog.accept runs, while it calls +aqt.exporting.ExportDialog.exporter.exportInto where +aqt.exporting.ExportDialog.exporter is some class of anki.exporting. + +Called from anki.AnkiPackageExporter._exportMedia each time a media is +exported. + +##### noteChanged +Called from anki.main.AnkiQt.noteChanged(nid), with argument +nid. Seems to never be called. + +### Network +##### httpRecv + +Contains a function recvEvent while some download occur. The hook is +removed when the download ended. + +The download may be either an add-on, or a synchronization. + +This method takes as argument a number of byt, add it to some count +`self.recvTotal` and then emitting something as a QT object. In +the case of synchronization, if the sync is aborted, it raises an +exception. + +It is called from anki.sync.AnkiRequestsClient.streamcontent when a +chink of data is received. This data may be either an add-on or a synchronization. + +##### httpSend +Similar to httpRecv, but for emissiond of synchronization, and not for +reception. + +Called from anki.sync._MonitoringFile.read. + +##### sync +Contains, equivalently: +* (lambda type:aqt.sync.SyncThread.fireEvent("sync",type)), +* (lambda type:aqt.sync.SyncThread.event.emit("sync",type)) +* (lambda type:aqt.sync.onEvent("sync",type)) +but only while aqt.sync.SyncThread.run runs, i.e. while anki synchronization is +running. Here, event is a pyqtSignal(str, str) + +It is called from anki.sync.Syncer.sync, each time the +synchronization's step change. The different step/type are, in this +order: +* login: just after saving the collection +* meta: startup and deletions +* server: stream large tables from server +* client: stream to server +* sanity: sanity check +* finalize + +Furthermore, there is the step/type "stream" called while streaming +occur in steps Server and client. + +It's not clear what is the goal of this, since the output of the hook +is never used, and the function does not have any access to the +synchronizer object. + +It's also called from anki.sync.FullSyncer.download, with the +following type: +* download +* upgradeRequired +and from anki.sync.FullSyncer.uplad, with the types: +* upload + +And from anki.sync.MediaSyncer.sync, with: +* findMedia. + +##### syncMsg +Contains aqt.sync.SyncThread.run.syncMsg, hence (lambda +msg:aqt.sync.SyncThread.fireEvent("syncMsg",msg)), hence (lambda +msg:aqt.sync.SyncThread.event.emit("syncMsg",msg)), where event is a +pyqtSignal(str, str). + +It contains this function only while the synchronization thread +runs. + +It's called from anki.sync.MediaSyncer.sync with a text stating how +many media remains to upload. And from +anki.sync.MediaSyncer._downloadFiles, where it states how many media +remains to download. + + +### Adding options +##### search +called from anki.find.Finder.__init__, with the argument +anki.find.Finder.search. This is a dictionnary from terms which can be +used in a search (in browser, in filtered deck) to a function giving a +sql query satisfying this search. This allow to add new terms and +function to this list. + +### fmod +The following four filters are called from +anki.template.template.Template.render_unescaped. When a field start +by `{{foo:`, i.e. it is `{{foo:bar}}`, the filter fmod_foo is +called on the content of the field bar. This may occur multiple time, +since multiple modifier may begin a mustache. + +For example, the add-on [Edit Field During +Review](https://ankiweb.net/shared/info/1549412677) add a hook +fmod_edit, so that you can use `{{edit:` in card type. + +##### fmod_kanji +##### fmod_kana +##### fmod_furigana +Contains methods anki.template.kanji, .kana, and .furigana. This edit +the text given in argument. Replacing   by spaces. And then some +transformation which I don't understand. It's supposed to be for Japan language. + +##### fmod_hint +Contains anki.template.hint.hint. A method which generate html for +hints link in questions. And the js code which replace the hint +link by the hint. + + + +#### Hooks called, but from methods which are never called themselves. +##### colLoading +Arguments are: self.col +It seems to be never called. More precisely, it's called in +aqt.main.AnkiQt._colLoadingState, which itself is never called. +##### mpvIdleHook +Contains aqt.main.AnkiQt.onMpvIdle. Not clear yet what it does. + +Called from anki.sound.MpvManager.on_idle, which seems to be never +called. + +##### reviewCleanup +Called from aqt.reviewer.Reviewer.cleanup, hence from +anki.main.AnkiQt._reviewCleanup assuming the new state is neither +"resetRequired" nor "review". Which itself seems to never be called. + + + +### Preparing question and answers +##### mungeQA +Contains anki.latex.mungeQA. A method which generate HTML which show +an image which contains the compilation result of the LaTeX given in input. + +Called from anki.collection._Collection._renderQA + +###### prepareQA +Called from aqt.reviewer.Reviewer._showQuestion, with the text of the +question, the card object and the text "reviewQuestion". + +Called from aqt.reviewer.Reviewer._showAnswer, with the text of the +answer, the card object and the text "reviewAnswer". + +Called from aqt.clayout.Reviewer._renderPreview, with the text of the +question, the card object and the text "reviewQuestion". + +Called from aqt.clayout.Reviewer._renderPreview, with the text of the +answer, the card object and the text "reviewAnswer". + +###### mungeFields +Called from aqt.collection._Collection._renderQA, after {{type: are +processed, Date: Sat, 15 Jun 2019 15:43:52 +0200 Subject: [PATCH 15/68] link to other docs --- documentation/links.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 documentation/links.md diff --git a/documentation/links.md b/documentation/links.md new file mode 100644 index 00000000000..36d4e9f010f --- /dev/null +++ b/documentation/links.md @@ -0,0 +1,11 @@ +Here are link to other nice documentation I found. + +# Anki droid +Ankidroid has a nice wiki, explaining many things. Not always up to +date. If I find some informations which fits in this wiki, I'll write +it in their wiki instead of this folder. + +In particular, they document most of the database structure, even if +some parts are still not clear. + +https://github.com/ankidroid/Anki-Android/wiki/Database-Structure From 70b4da0ce75b1f93c05df0b4e5854f3b9605b999 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Sat, 15 Jun 2019 15:44:30 +0200 Subject: [PATCH 16/68] Documenting previewing cards --- documentation/preview.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 documentation/preview.md diff --git a/documentation/preview.md b/documentation/preview.md new file mode 100644 index 00000000000..2abbba3fd60 --- /dev/null +++ b/documentation/preview.md @@ -0,0 +1,22 @@ +# Here are the rules governing previewing card: + +You can preview cards. There are two kinds of previewing. As far as I +know, the difference is not explained anyone yet. + +## Normal previewing +The first kind of previewing allow you to see the result of applying a +template to a cards. This is what you obtain when you click on the +button `Cards...` if you are viewing a card already created (i.e. in +the browser or in the Edit window). It is also what you obtain when +you click on preview in the browser, and when you actually review a +card. + +## Filled previewing +When you click on the button `Cards...` from the Add window, ord +from the Note Type manager, you enter this mode. In this mode, every +fields are filled. Either by the actual content of the card you are +created, or by the name of the field parenthesis. + +It means that you can not test the result when some fields are missing +using this window, because the content of `{{^foo}}...{{#foo}}` +will never be displayed. From 0ff5d9770d182b9592e7119543cb13b4783c8765 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Sat, 15 Jun 2019 15:44:39 +0200 Subject: [PATCH 17/68] documenting collection's profile column --- documentation/profiles.md | 101 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 documentation/profiles.md diff --git a/documentation/profiles.md b/documentation/profiles.md new file mode 100644 index 00000000000..db481bdcf2a --- /dev/null +++ b/documentation/profiles.md @@ -0,0 +1,101 @@ +This document describe the content of the file profiles.db +(unpickled). It contains a line _global, and a line by profile. Both +are described separately. + +# Global +## ver +## firstRun +## created +## defaultLang +## updates +## suppressUpdate +## disabledAddons +## lastMsg +## id + +# Profile + +## mainWindowGeom +## mainWindowState +## numBackups +## lastOptimize +## fullSearch +## searchHistory +## lastColour +## stripHTML +## pastePNG +## deleteMedia +## preserveKeyboard +## syncKey +A key, given by the server during a synchronization. It is used +instead of the password. This allow to avoid to save the password in +the computer. + +Note that this key allows to synchronize, so it is almost as powerful +than the password. The only things that can not be done with it are +the ones related to ankiweb only. I.e. sharing decks and add-ons, and +deleting the account. + +## syncMedia +Boolean. whether media should be synchronized. It can be decided in +the profile option. + +## autoSync +## allowHTML +## importMode +## getaddonsGeom +## addGeom +## editor3Splitter +## editorGeom +## editorState +## editorHeader +## studyDeck-defaultGeom +## studyDeck-selectModelGeom +## mediaDirectory +## studyDeck-selectDeckGeom +## editcurrentGeom +## syncUser +The login (i.e. email). +## deckconfGeom +## previewGeom +## checkmediadbGeom +## findreplaceGeom +## hideDeckLotsMsg +## importDirectory +## changeModelGeom +## CardLayoutGeom +## getTagGeom +## modelsGeom +## emptyCardsGeom +## modeloptsGeom +## exportDirectory +## hostNum +Initially None. Else a number sent during synchronization. The +synchronization is done with https://sync{hostNum}.ankiweb.net, except +the first time where it is https://sync.ankiweb.net. + +## mediaState +## importState +## revlogGeom +## dyndeckconfGeom +## ViewHTMLGeom +## deckStatsGeom +## nm_user_color_map +## nm_color_a +## nm_style_scroll_bars +## nm_invert_latex +## nm_mode_settings +## start_at +## end_at +## nm_enable_night_mode +## nm_state_on +## nm_disabled_stylers +## nm_color_t +## nm_transparent_latex +## nm_color_b +## nm_color_s +## nm_invert_image +## nm_enable_in_dialogs +## addonsGeom +## addonconfGeom +## addonconfSplitter From 8f18d15dd9f7a7efa92be9a8f9a7eb97ffb64120 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Sat, 15 Jun 2019 15:44:52 +0200 Subject: [PATCH 18/68] documenting sync process --- documentation/synchronization.md | 346 +++++++++++++++++++++++++++++++ 1 file changed, 346 insertions(+) create mode 100644 documentation/synchronization.md diff --git a/documentation/synchronization.md b/documentation/synchronization.md new file mode 100644 index 00000000000..98b7982f915 --- /dev/null +++ b/documentation/synchronization.md @@ -0,0 +1,346 @@ +In this file, I present anki's synchronization process. I first +describe the general mechanism used in anki's code (desktop +version). I then describe each step. + + +# Protocol +Each transmission uses post. The method `foo` send its request to +either https://l1sync.ankiweb.net/{m}sync/foo, or +https://sync{d}.ankiweb.net/{m}sync/foo, where `{d}` is a value provided by the +server during the previous sync (initially the empty string), and {m} +is "m" if and only if it's a media synchronization, otherwise, it is +the empty string. + +The header is a dictionnary containing: +* 'Content-Type': 'multipart/form-data; boundary=%s' % "Anki-sync-boundary".decode("utf8"), +* 'Content-Length': the size of everything sent in the body + +The body contains, separated by "--Anki-sync-boundary",: +«Content-Disposition: form-data; name="{key}" + +value +» +for a dictionnary of pair key-value. Those values describe the +message sent (i.e. key to connect, identification number, whether +compression is not used). But does not contain the actual data (unless +the actual data is the key, when a connection should be established). + +If the protocol requires to sent an object, then it is sent as +« +Content-Disposition: form-data; name="data"; filename="data"\r\n\ +Content-Type: application/octet-stream\r\n\r\n"» +{object} +» +Object is build as follow: +* object is dumped into json +* result is encoded in UTF8 +* io.BytesIO is applied to it.s +* compressed with gzip, compression level 6 + +The message then ends with «-- +» + + +## Normal synchronization +Each message sent in the following protocol send at least the +dictionnary containing: +* 'c': whether the object sent is compressed with gzip. +* 'k': the hkey, +* 's': a random string of 8 chars + +With one exception, the hostkey post request does not contain the two +last values (since this request allows to discover those values). +### HostKey +The hostKey is an identifier given by the server which replace the +pair login/password. It is saved by anki when you sync. If you don't +have an hkey, anki asks to your login/password, and begin by sending a +request for an hostKey. + +The post request `hostkey` request contains no post objects. Its +objects is the dictionnary with: +* 'u': the user login +* 'p': the password. + +It return the hostkey. + + +### Meta +The post request `meta` request contains as object the dictionnary: +* 'v': SYNC_VER (currently 9, but I assume it may change when anki + version change) +* 'cv': "ankidesktop,{versionWithBuild},{}" + +It should return a dict containing: +* msg: An arbitrary message to show to the user at the end of the + sync. Usually the empty string. +* cont: if its falsy, it means the server abort. +* scm: a schema modification time. If it's not equal to the + collection's scm value, it mean that some part of the collection was + modified (note type, card type) since last sync, and thus full + update is required. +* ts: time stamp. The time according to the server. If the difference + in time between server and computer is greater than 5 minutes, the + synchronization is rejected. (Time zone are of cours taken into + account) +* mod: the time of the last modification on the server. If it's value + is equal to mod on the computer, it means that no change did occur, + thus sync is not required. +* usn: Unique synchronization number. The greater usn of some anki + object on the server is usn. Thus, the next modification will have + synchronization number usn+1, to emphasize the state that it occurs + after this synchronization. +* uname: (equivalent to syncName in the profile). The user name (i.e. login/email). It must be sent because the user + may have chaged it on the server. +* hostnum: as in the profile. Its a value indicating which url/server to + use for synchronization with anki. + +### Basic check +The database will check that everything is consistent before +resuming. This is the first time it will occur. This check is +described in [BD_Check.md]. + +Hook ("sync","meta") is now called, but seems to have absolutly no +effect. + +### Start +The post request `start` is sent with object a dictionnary: +* 'minUsn': the collection's value USN. I.e. the last number sent by + the server (plus one). It represents the minimal USN of change that + need to be downloaded. +* 'lnewer': whether collections's mod is greater than server's mod. + +It return a dictionnary associating to each key a list of ids of +things to delete. Keys are notes, cards and decks. The content of the +deleted deck is not deleted, nor are the children of the deck if it +has any. + +### applyGraves +The post request `start` is sent with object a dictionnary with a +unique key 'changes' whose value is a dict with keys "notes", "cards", +and "decks", each containing ids of object deleted since last +synchronization. At most 250 are sent, the command may be sent +multiple time if required. + +### applyChanges +The post request `start` is sent with object a dictionnary with a +unique entry "changes", whose value is a dict with keys 'models', +'decks', 'tags' as they are saved as dict in the database. If mod is +newer here than on server, then the dict in the dict also contains key +'conf' and 'crt'. + +It returns a dictionnary with: +* models: container of model (note type) dictionnary. +* decks: a list of two elements +** the decks, as in the case of models +** the dconf (deck options). As in the case of models. +* 'tags': the container of tags added. +* 'conf': (not mandatory. Probably only here if it did indeed changed) +* 'crt': as conf. + +In each case, it sends only thing which changed, which may have to be +replaced in the collection. It is possible that the replacement does +not occur if the model have also been modified in the collection and +that the time of this modification is after the time of the +modification saved on the server. + +### chunk +The post request `chunk` does not send anything. It receive a +dictionnary (the chunks) containing some keys in 'revlog', 'cards' +and 'notes', containing lines to enter in the table of database with +the same name (unless an entry with the same id is already here). + +It also contains `done`, stating whether there are no more thing to download. + +This post request is repeated until chunk['done'] is truthy. Each time, +hook ("sync","server") is called. But seems to have absolutly no effect. + +### stream +The post request `applyChunk` takes as input a dictionnary, similar +to the one receveide by the `chunk` method. I.e. containing +keys `revlog`, `cards`, `notes` and `done`. This dic contains at most +250 elements. + +This post request is repeated until there are no more new lines to +sent. Each time, hook ("sync","server") is called. But seems to have +absolutly no effect. + +### sanityCheck + +Hook ("sync","sanity") is called. It changes the message to +"Checking...". + +It calls basic check and then ensures that USN should not be -1 in +cards, notes, revlog, graves, deck, tag, model. If it is the case, +return a string giving the name of the table not satisfying it. + +If a non-root deck has no parent, it is created. + + +It computes the list `c` +* three numbers to show in anki +* list/footer. I.e. Number of new cards, learning repetition, +* review card. (for selected deck), +* number of card, +* number of notes, +* number of revlog, +* number of grave, +* number of models, +* number of decks, +* number of deck's options. + +### sanityCheck2 +The method `sanityCheck2` is now called. It sends as objects +result `c` from sanityCheck. It returns a dictionnary `dic` +which contains a key 'status'. Two paths are then taken, depending on +whether `dic['sync']` is the string 'ok' or not. + +I assume taht `ok` occurs if the server, doing the same +computation, finds the same result. If so, it is possible that a sync +fail if it is done almost at time of new day, since the number of card +to review would be distinct on server and on computer. + +#### Finish (If it's 'ok') +runHook("sync", "finalize") is called. It does nothing. + +The method `finish` does not sent any object. It returns a number, +which will become collections's `ls` value. + +Collection _usn value is incremented to be maxUsn+1. + +Database is considered to be modified. And then collection is saved. + +#### If it's anything but 'ok' +Collection is rollback. State schema is modified. Save the collection + + +## Media Syncing +A special method exists for test. We do not consider it in this +document. + +Except for the `begin` command, the dictionnary of value sent contains a single element: +* 'sk': whose value is an identification number returned by the server + to be used during this connexion. + +### Finding media changed +hook("sync","findMedia") is run. The window's text become "Checking media...". + +### begin +The first method used id `begin`. Its dictionnary is: +* 'k': the hostkey +* 'v': "ankidesktop,{anki's version number},{platform}:{platform's + version}", with platform being either win, lin, mac or unknown. In + the last case, there are no version sent. + +It receive a json utf8 dictionnary with: +* identification number. This number is sent back to the server with + each next communication. +* usn: date of last modification of medias on the server + +if server's usn is equal to collection's greatest usn, no change are +found and the sync halts. +### mediaChanges +The method `mediaChanges` send the last USN of medias in the +collection. + +It returns a list. Each element of the list is a triple: +* name, the name of the media file which is believed to be new or + changed on the server. +* usn, date of the last change of this file +* sum. Either a falsy value is the media is deleted. Or a checksum of + the file to test whether two files are equal. + +Those elements are probably in non-decreasing order of USN. + +Each such media is logged. If the media is new or changed on server, +it is added to the list `need`. If the media existed and was +dirty, it is cleaned. + +If the media is deleted on the server and not dirty, then it is also +deleted in the collection. Otherwise, if it is dirty, it log +"conflict; will send", but actually do nothing. + +If both sides see the data as deleted, log this information and clean +it on the collection side. +### downloadFiles +The method `downloadFiles` sends a dict with a unique key 'files', +whose value is the first 25 files which must be downloaded. + +Returns a zip file containing: +* _meta, a file containing a json dict associtaing to each name of file in zip (except meta) a name to be used in the media folder +* arbitrary fields to save in the media folder + +The hook "syncMsg" is run, with a message stating how many files were +downloaded. It changes the message on the progress window. + +This is done until there are no more files to download. + +### LastUsn +Change the last usn of media to the Usn of the last file announced in +data. + +### uploadChanges +This step occurs as long as media must be sent. +Hook ("syncMsg") is run and state how many media still need to be +sent. + +The post method `uploadChange` contains as object a zip file, +containing: +* from 1 to 25 files +* a file _meta, associating to each file name in the zip directory + another name to be used in the collection. I assume that the name in + a zip file are limited and this allow to still have any name + accepted on the operating system. + +Return a pair with: +* the number of file processed, +* the last usn on server which become the collection's new lastUsn in + the database. The db is then sync. + + + +This is done in loop until there no more data to send. + +If at some step, the change of last usn is not equal to the number of +media sent, it means that concurrent update occured. This is +logged. When the entire syncing process ended, it is entirely +restarted. + +### mediaSanity +The post request `mediaSanity` sends a dic with a unique key +`local` whose value is the number of media. It returns a +string. This string is returned by the method sync. If the string is +not "OK", the media database is emptied by method +`anki.media.MediaManager.forceResync`. + +## Full upload + +run hook ("sync", "upload"). It change the text to "Uploading to +AnkiWeb...". + +It check whether the database is in correct state, and basic check +(see [DB_check.md]). In case of problem, no upload and return False. + +* change usn -1 to 0 in notes, card and revlog, and all models, tags, decks, deck options. +* empty graves. +* Update usn +* set modSchema to true (no nead for new upload) +* update last sync time to current schema +* Save or rollback collection's db according to save. +* Close collection's db, media's db and log. + +The method is `upload`, it contains as object the collection database. + +## Full download + +run hook ("sync", "upload"). It change the text to "Downloading for +AnkiWeb...". + +The post method `download` takes no deal argument. It may returns +the string is "upgradeRequired", the hook is called ("sync", +"upgradeRequired") and halts. It show the message "Please visit +AnkiWeb, upgrade your deck, then try again." + +Otherwise, it returns a sqlite database. It is checked to be a +database readable with at least a card. If it has no card and current +collectio has card, then nothing the downloaded db is deleted, +otherwise it becomes the collection's database. From 1933bafdc6a3c7e2c00842711f7f1e446b0b965a Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Sat, 15 Jun 2019 15:44:58 +0200 Subject: [PATCH 19/68] documenting how to decide which cards are generated --- documentation/templates_generation_rules.md | 86 +++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 documentation/templates_generation_rules.md diff --git a/documentation/templates_generation_rules.md b/documentation/templates_generation_rules.md new file mode 100644 index 00000000000..cc816c336c4 --- /dev/null +++ b/documentation/templates_generation_rules.md @@ -0,0 +1,86 @@ +# How to decide whether a card should be generated/deleted + +Anki's official +[documentation](https://apps.ankiweb.net/docs/manual.html#conditional-replacement) +is not really clear about the rules governing the generation of +cards. It states +> wrapping a template in `{{^Field}}` will not do what you expect. + +But it let us guessing which are the cases creating trouble and which +cases are ok. + +This document explains which are the rules which decides when a card +is generated or not. Those rules are implemented in +[models.py](../anki/models.py), in the method `_reqForTemplate`. + +I assume that those rules are created in order to make anki quicker, +and less expensive in computation time. Alas, it makes anki more +complicated for power user. + +## The rules + +### Non cloze models + +Anki generate the html content of some cards in some cases. It checks +this content to choose what kind of rules should be applied + +First, anki generates the html of card where all fields are +empty. We'll call this content `empty content`. We'll use this +twice below. + + +#### Everything and Nothing +Anki generates the content where every fields contains "ankiflag". It +then checks whether the result is equal to `empty content`. +Intuitively, if you have the same result when everything is filled and +when everything is empty, it probably means that the template does not +consider its input. Anki then consider that this template can be +discarded. + +In this case, the method `_reqForTemplate` returns `("none",[],[])`. + +This is why the documentation state that you should not put everything +inside `{{^Field}}...{{/Field}}`. If you do that, then each time +the field `Field` is filled, the html content of this card is +empty. And thus anki believes that the note never generate any +content. + +In particular it means that no card with this note type will ever be +generated. + +### Removing one field +Now we know that some fields are actually used in the template, +and that, if every fields are filled, we have some content. Now, we +consider what happens if a single field is missing. + +Thus, we generate the html content, when a field `Field` is empty, +and every other field contains the text "ankiflag". If we also find +"ankiflag" in the result, it means a field was shown, thus `Field` +is not mandatory. + +If we don't find "ankiflag" in the result, we consider `Field` to +be mandatory. + +If there is at least one mandatory field, `_reqForTemplate` +returns the pair `("all",l)` where `l` is the list of +mandatory flags. + +In this case, cards are generated for this template if and only if all +of those "mandatory" fields are filled. + +### Using a single field +We now assume that no fields are mandatory. In this case we check for +fields which are sufficient by themselves to generate a card with some +content. + + +Thus, we generate the content, when a field `Field` contains "1", +and every other fields is empty. If the result is not the same as the +html `empty content` computed above, then we consider that the +field `Field` is sufficient to generate the card. + +Then `_reqForTemplate` returns `("any",l)` where `l` is +the list of sufficient field. Note that the list may be empty. + +In this case, cards are generated for this template if and only if all +of those "mandatory" fields are filled. From 3636807bc7fd4f441e164be998ba71de77bbe89d Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Sat, 15 Jun 2019 15:45:33 +0200 Subject: [PATCH 20/68] Glossary --- documentation/glossary.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 documentation/glossary.md diff --git a/documentation/glossary.md b/documentation/glossary.md new file mode 100644 index 00000000000..63dcf2b7014 --- /dev/null +++ b/documentation/glossary.md @@ -0,0 +1,37 @@ +Here is the documentation of vocabulary found in anki code and not in +anki's documentation. Because it's mostly internal stuff. + +# Collection +## Deck conf +That's one of the option which can be applied to a deck in the main +page. + +## Model +What code calls model is called «note type» in Anki's +documentation. The name model is still found in the synchronization +protocol, and in «mid», which represents id of note type. + +## Template +What code calls template is called «card type» in Anki's +documentation. + +## id's +The idea of an object is often denoted as `Oid`, with O the +initial of the kind of object. Thus cid, nid, mid, did and oid, for +ids of card, note, model, deck, and original deck. + +However, in the object itself, the id is denoted as `id` and not +as `Oid`. For example, the id of a card is `card.id`. + + +# Main window +The main window may display different kind of content. The «state» of +the main window is the kind of content on it. + +* Reviewer: The window with a card, and button to answer. +* DeckBrowser: The list of decks +* sync: waiting for sync to finish (the content is the same as + deckbrowser) +* overview: what you obtain when selecting a deck but not yet studying it +* profileManager: no window, instead ask to choose for a profile +* startup: empty window, before collection is loaded. From b5785195e47e04e0e458c8a5ba75f12f7894f983 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Sat, 15 Jun 2019 15:54:11 +0200 Subject: [PATCH 21/68] delete and new same file ? Don't understand git --- designer/icons/media-record.png | Bin 31 -> 727 bytes 1 file changed, 0 insertions(+), 0 deletions(-) mode change 120000 => 100644 designer/icons/media-record.png diff --git a/designer/icons/media-record.png b/designer/icons/media-record.png deleted file mode 120000 index 0e95c2bfdef..00000000000 --- a/designer/icons/media-record.png +++ /dev/null @@ -1 +0,0 @@ -../../web/imgs/media-record.png \ No newline at end of file diff --git a/designer/icons/media-record.png b/designer/icons/media-record.png new file mode 100644 index 0000000000000000000000000000000000000000..776e38f6a605b1ab654a3eb07fc508cd134388c4 GIT binary patch literal 727 zcmV;|0x127P)AaVOc0nLY!EhRjpRtkb%O7(4~O`q`!R{b^PTOxvlon3xYS(g zMrxwkE>cfYf21DSi^vtSk@|Z_TlMT@J9`~8O&MfkZcHnH$H(swYDq#m(*{=3Qf5i=wWQG+WWMVtF-eC0z5QV(NFIpEiVOg@7$r547^i_dR69Gg8RpyW>i3Lt9-QR~CEF-T2rt@wffX@wq2UYTrA^)S54AqoNz~J~y5A%^{QPs`@ z&|ygcTv~n;UFl4ieI=0-yYpB{Y4^w?Q8D`>c}-ZvD}B|zGJut{0IXa3u2*q_xm*D} z$w{g@wi-5?YbTBidac&KU_`a+8sJ9U(Kgj-9+dkm@Ifn+uAz0@52il$8d^qEs6!XzFhd=o1xSEFOeTl@%d#=xit$K!b%3_Wv+|w5Ei)nuOh| zg1N`FV}5hungw{HC-D$VPvUl7s=yzyctf@2xVCtq;9E;;3O1n|c-Xv}Y+sFMOA$B% zN8kt?fdi5i@;#1-Yi)vwH6n8|WnbiF9!eF_r6X_zo{#(qFaV$&bY~{&6?Oms002ov JPDHLkV1l9sLva8A literal 0 HcmV?d00001 From 8fb8dc89469778c5ada6fddb3a3d194bcdf5f6df Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Sat, 15 Jun 2019 15:37:41 +0200 Subject: [PATCH 22/68] Describing files in each folder --- FILES.md | 88 ++++++++++++++++++ anki/FILES.md | 195 ++++++++++++++++++++++++++++++++++++++++ anki/importing/FILES.md | 46 ++++++++++ aqt/FILES.md | 173 +++++++++++++++++++++++++++++++++++ 4 files changed, 502 insertions(+) create mode 100644 FILES.md create mode 100644 anki/FILES.md create mode 100644 anki/importing/FILES.md create mode 100644 aqt/FILES.md diff --git a/FILES.md b/FILES.md new file mode 100644 index 00000000000..4ed2b4c4811 --- /dev/null +++ b/FILES.md @@ -0,0 +1,88 @@ +This document describe the different files and folders you'll find in this +folder. Files in subfolder are described in the file FILES.md in this +subfolder. I also indicate which files are in original anki source +code, and which are added by the author of those comments. + +This should helps an (add-on) developpers, by letting them know where +to search for informations. + +# Folders + +## [anki](anki/FILES.md) +This folder contains most of the back-end. That is it contains +everything related to the database and its content. It allows to +manipulate decks, decks' option, cards, notes, note type, templates, +tags, and so on. It also deals with synchronization. + +## [aqt](aqt/FILES.md) +This folder contains most of the controller. It contains methods for +each possible actions in anki. It also contains the values associated +to each windows. It does not contains the actual code used to create +the window. However, it contains the code used to enter informatinos +into the window. + +Note that running tools/build_ui.sh add a subfolder aqt/forms/. This +folder contains code compiled from designer/ which generates the window. + +## designer +This folder contains the windows description, as .ui file, (a format +related to pyqt). When running tools/build_ui.sh, its content is +compiled into python code and sent into aqt/forms/. Those new files +are used to generate the window. + +This folder also contains some images used by the GUI but not by its +HTML part. + +## tests +As the name suggest it, this folder contains tests. Any change made to +anki must not break those tests. As any tests, they are not exhaustive +and a change may break them even if they do not detect anything. + +## tools +Some script. Most of them are used to compile anki. The only exception +is tools/runanki.system.in. A copy of this file is used to start anki +from source. + +## web +This title is kinda misleading. This file is not actually used for +ankiweb, nor for anything related to internet. + +An important part of anki's user interface is coded in +html/css/javascript. The javascript and the python may interact and +call functions created in the other language. The python generate the +html, which imports the css, js and images files from this folder. + +# Files +I do not any markdown file, readme file, makefile, and git +related file. + +## anki.1 +The unix manual (man) page + +## anki.desktop +Description of anki used by the Gnome's window manager in UNIX +system to create an entry on the desktop. It may be used by any window +manager reading .desktop file. + +## anki.png +Anki's logo + +## anki.xml +Mime information. It states that .colpkg and .apkg files are anki's +file. + +## anki.xpm +A X pixMap file, used to represent anki's logo in pixel art. + +## pkgkey.asc +A public pgp key. It's not clear where it is used, if anywhere. + +## requirements.txt +The list of python library used by anki. Used during installation from +source, during the step ```pip3 install -r requirements.txt```. + +## runanki +A python file allowing to start anki from source. + +## .travis.yml +A Yaml file ensuring that Travis know how to integrate anki. diff --git a/anki/FILES.md b/anki/FILES.md new file mode 100644 index 00000000000..7cd585286fd --- /dev/null +++ b/anki/FILES.md @@ -0,0 +1,195 @@ +As the file with [same name in parent directory](../FILES.md), this +document contains a succint description of the content of each file +from this folder. + + + +# Folders +## Template +This folder deals with the template language used by anki's card's +type. This language, also called mustache, which uses a lot of {{ and +of }}. + +The content of this folder as another copyright than anki. + +## Importing +Deal with importing files into anki. The file may be either created by +anki or by another program + +# Files +We consider files related to the entirety of anki, such as database +and constants. And then files which encode a particular part of anki +such as note and decks. + +## Anki-wide files +The files considered in this section are not related to the data of +some particular profile. + +### __init__ +This ensure that python is at least 3.5, with UTF-8 locale. It +contains anki's version number. + +Note that ```from anki import *``` imports ```anki.storage.collection```. + +### consts +This file contains constants. Those constants are used in any other +files. They also are used to encode/decode the content of the +database. + +### Error +This file contains two kind of exceptions. A general AnkiError +exception. And DeckRenameError. + +### Exporter +This file contain a main class ```Exporter```. This class is inherited +by other classes which allow to export's anki cards, note, deck or +package. Exporting is done with or without meta data. Those meta data +are, for example, the decks' name and note's type. Exporting with +metadata create a sqlite database. Exporting without metadata create a +csv file. + +### Hooks +This file contains functions used to create and run hooks and +filters. It can also be used to "wrap" functions, as explained in +anki's add-on's documentation. + +### Langs +This file contains functions used to localize anki in different +language. It does not contains the actual translation. + +### Latex +This file contains functions used to compile LaTeX from the notes' +field into images. + +### Mpv +This file contains the code used to read and record sounds. This +contains the part related directly to the MPV player. + +### Sound +This file contains functions used to find the sound(s) to play in +notes' field. It also controls mpv. + +## Utils +This class contains a lot of methods used to do transformation on +data. Those transformations may be used in any other classes. + +## Files used to represents some a single value +Most files contains a single are associated to a notion of interest in +anki. Examples of such notions are cards, notes, decks, note +type... Some files contains a second kind of notion, associated to the +first one. For example (decks.py) also contains the values represents +decks' option. + +Each of those files define a single class, used to manipulate those +values. We subdivise this section in two, depending on whether one or +multiple instances of this class exists simultaneously. + +### Class with many instances +Each value we consider in this section is encoded in python as an +instance of a class. Those class are defined in the files presented in +this section + +#### Cards +This file define a class Card. An instance of Card represents an +anki's card. It serves to create new cards, and find cards from the +collection. + +#### Notes +This file define a class Note. An instance of Note represents an +anki's note. It serves to create new notes, and find notes from the +collection. + +#### db +This file contains a single class, DB. Each instance of DB represents +a connexion to a sqlite database. Queries are not made directly to the +database, but through an instance of DB. For example, it ensures that +if some values is written in the database, the collection is marked to +be modified, and a synchronization is planified. Some other methods, +such as ```rollback```, only consists in calling the method with the +same name on the underlying database. + +#### Stats +Go see "Stats" in the following section. + +### Class with a single instances +The files in this section allow to encode some values. Those values +are encoded as dictionnaries. Each file define a single class, which +define method to edit those dictionnaries. Those classes are called +manager. Each instance is associated to a collection. Thus a new +instance is created each time the profile is changed. + +While it is not clear why anki's author used manager. Why values are +dictionnaries and not object. The best guess of the documentation's +writer is that it corresponds to the way to save data in the +database. Each data considered in this section is saved as a json +string in the database. + +#### collection +This file contains a single class _Collection. A collection represents +a profile. Each time the user is changed, the collection is closed and +a new collection is opened. + +This class contains a lot of paramaters related to the profile such as +the profile's id. It contains also a lot of method to create or get +any kind of values. For example, ```newNote``` allow to create a new +note, associated to this collection. And ```getNote``` allows to +obtain a note from the collection. Note that those could be done +directly using the class Note, but the collection must be passed as +argument when the class is created. Using those functions allows to +avoid doing this. + +#### Decks +This file contains a single class, ```DeckManager```. This of course +deals with decks. Each deck being a single dictionnary. The +DeckManager also deals with the decks' option, also known as "configuration". + +#### Finder +A finder object allow to find cards or notes satisfying some +query. The query is the same as the one used in the browser and in +filtered decks. + +#### Media +This file contains a class MediaManager. This class's instance deal +with medias. It update the media database and the media folder of the +current collection. + +#### Models +This file contain a single class, called ModelManager. A note type, +also called model in the code, is encoded as a dictionnary. Each card +type, also known as template in the code, is also encoded using a +dictionnary. This class allow to get and change models. + +#### Sched and schedv2 +This fill contain a single class Scheduler. The instance of the +scheduler has two purposes. It allow to find the following card to +study. And when a card is studied, it reschedule it. + +Each file correpsonds to a distinct scheduler. A big part is common to +both scheduler and could actually have been merged into a single +class. + +#### Stats +This deck contains two subclass. CardStats and CollectionStats. Each +class allow to compute and show statistics of review of a single card +or of the whole collection. Note that the first class should have been +in the previous section and not in this one. + +#### Statsbg +The stat bacground image, encoded in base64. + +#### Stdmodels +This class contains function to create the basic models. It is used to +initiate the collection. It is also used to import notes which were +exported from another software. + +#### Storage +This file contains a single class Collection. This is used to create a +new collection or open it from the database's profile. + +#### Sync +This class contains code used to synchronize the collection with the server. + +#### Tags +This class contains a single class called TagManager. It is used to +edit the set of tags saved in the collection. It does not deal with +the tag in a single note. The collection save tag in order to autocomplete. diff --git a/anki/importing/FILES.md b/anki/importing/FILES.md new file mode 100644 index 00000000000..b77b1dc9de7 --- /dev/null +++ b/anki/importing/FILES.md @@ -0,0 +1,46 @@ +As (../../FILES.md), this document contains a succint description of +the content of each file from this folder. + +# __init__.py + +This only contains Importers, a list of pair, associating to each +description the importer object which allow to import this kind of +file. + + +# base +This file contains the definition of the class Importer. This class is +then inherited to allow the importation of any kind of files. + +# Kind of files to import + +## apkg +This is used to import a apkg files. Those are files created when +exporting a deck in anki. This is actually used for .apkg, .colpkg and +.zip files. + +### anki2 +If the package was generated with anki2 and not 2.1, the importer used +comes from this file. + +## csvfile +This file contains code used to import a CSV file. Note that this does +not contains the code used to generate the window allowing to +configure the importation. The extension of a CSV file may be anything. + +## pauker +This is used to import lessons from Pauker 1.8. Those are .pau.gz +files. + +## Mnemo +This contains the code used to import Mnemosyne files. This file is +supposed to be a sqlite database with extension .db + + +## supermemo_xml +As indicated by the name, this file contains an importer for xml files +generated by supermemo, with extension .xml + +## noteimp +This file allow to import a note. Those notes are supposed to have +been outputted using notes in plain text. diff --git a/aqt/FILES.md b/aqt/FILES.md new file mode 100644 index 00000000000..56fdd18a486 --- /dev/null +++ b/aqt/FILES.md @@ -0,0 +1,173 @@ +As the file with [same name in parent directory](../FILES.md), this +document contains a succint description of the content of each file +from this folder. + +The only folder is forms, which is obtained by compiling +(../designers). Thus, it content won't be described here. The files in +this folder does not contains the code to describe the QT window. It +sometime contains HTML content to put in those windows. And it +populate the window's value + + +We first describe windows. We then describe everything else +# Windows +## About +This file deal with the "Help>About" window + +## AddCards +This file deal with the window opened in order to add a new note in +anki. It thus also add cards (and in fact rejects to add a note +without cards). It use the Editor as a subwindow. + + +## Addons +This file deals with addons. More precisely, it deals with: +* The add-on manager. +* The edition of add-on's configuration +* Downloading a new add-on + +## Browser +This file deal with the browser. It use the +Editor as a subwindow when a single card is selected. + +## clayout +This file contains a single class CardLayout. It describes the way to +present a card. Either during review, preview, and edition of card's +type. + +## customstudy +The window "custom study". When a deck is selected but reviewing is +not yet started, you can open this window to review more cards. + +## deckBrowser +The content of of the main window when it show decks. + +## deckchooser +The window allowing to choose a deck. Either to choose were new cards +are added, or to choose where to move an existing card. + +## deckconf +The window "option". Obtained from the main window by clicking on the +gear and then Options. + +## DynDeckConf +The window used to create and edit a filtered deck (also known as +dynamic deck) + +## EditCurrent +The window used to edit the card currently being reviewed. It use the +Editor as a subwindow. + +## Editor +The subwindow used to edit the content of a note. It is used in the +browser when a single card is selected, in editcurrent and in addcard. + +## Error +The window which show when anything was written on stderr. + +## Exporting +The window opened to export cards or notes. It can be opened by +File>Export from the main window, or by selecting a deck's gear and +then Export. + +## Fields +The window which allow to add/remove/rename fields from a note +type. It can be opened in the editor, or in the note's type manager. + +## Importing +The windows for importing cards. It is not the window to find the +file, but the window to deal with it. + +## Main +The first window opened when the profile is loaded. It may contain +either the list of decks, a deck's overview, or the card being +reviewed. It always contains Toolbar + +## mediasrv +TODO + +## ModelChooser +The window which allow to choose a note's type (a.k.a. a model in +anki's code). +It also deals with the button used to open this window. +It can be opened in the addCard's window, note importer, +and when changing a note's type. + +## Model +The window allowing to change a notes type (a.k.a. a model in anki's +code). This is obtained from any editor or from the note's managare by +clicking on "cards". + +## Overview +The content of the main window when a deck is selected but review did +not start yet. + +## pinnedModules +Allow to keep some python packages that are commonly used by add-ons, +but no longer used by Anki itself, available in the official builds. + +## preferences +The preference for the whole collection. Obtained from the main window +by "Tools>Preferences". + +## Profiles +The profile manager. The first thing opened by anki. And obtained +again by "File>Switch profile" + +## Progress +The window showing how much an action progress. Used while checking +the database, the media, the empty cards, etc... + +## Reviewer +The content of the main window, when a card is beeing shown. + +## Sound +The window used to record a sound + +## Stats +The stat window. Obtained by pressing the button "Stats" in the main +window. + +## StudyDeck +The window opened by "Tools>Study deck" (shortcut /) allow to start + reviewing the selected deck. + +## Sync +Window showing the progress of the synchronization + +## TagEdit +An editor of card. Not clear which one. TODO + +## TagLimit +The window obtained by selecting a normal deck, then Custom +Study>Study by card state or tag>Choose tags. + +## Toolbar +The line on top of the main window, with "Decks, add, browse, stats, +sync" + +## Update +Message to check whether anki can be updated or not. + + It can also +be opened from card adder. And from the browser in order to move a +card to another deck. + + + +# else Everything +## downloader +Used to download add-ons. It does not corresponds to a window. + +## Qt +Ensure that QT has the correct version. QT's function are used +through this file. + +## Utils +A lot of tools used by many window for standard actions to do. + +## WebView +Page to show content encoded as HTML. + +## WinPath +Find path in windows. From 84ad88083cfbad7b094f268b04848370679eb526 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Sat, 15 Jun 2019 15:49:31 +0200 Subject: [PATCH 23/68] commenting python code --- anki/cards.py | 136 +++++++++++-- anki/collection.py | 328 +++++++++++++++++++++++++----- anki/consts.py | 2 + anki/db.py | 22 ++ anki/decks.py | 288 +++++++++++++++++++++++--- anki/exporting.py | 17 +- anki/find.py | 51 +++++ anki/hooks.py | 32 ++- anki/importing/__init__.py | 1 + anki/importing/noteimp.py | 27 ++- anki/latex.py | 84 +++++++- anki/media.py | 120 ++++++++++- anki/models.py | 283 ++++++++++++++++++++++++-- anki/notes.py | 75 ++++++- anki/sched.py | 360 +++++++++++++++++++++++++++++++-- anki/sound.py | 9 + anki/storage.py | 6 +- anki/sync.py | 402 +++++++++++++++++++++++++++++++------ anki/tags.py | 23 ++- anki/template/LICENSE | 0 anki/template/README.anki | 0 anki/template/__init__.py | 9 + anki/template/furigana.py | 1 + anki/template/hint.py | 8 + anki/template/template.py | 40 +++- anki/utils.py | 28 ++- aqt/__init__.py | 52 ++++- aqt/addcards.py | 24 +++ aqt/addons.py | 141 ++++++++++++- aqt/browser.py | 66 +++++- aqt/clayout.py | 57 +++++- aqt/deckbrowser.py | 23 +++ aqt/downloader.py | 12 +- aqt/editcurrent.py | 6 +- aqt/editor.py | 69 ++++++- aqt/exporting.py | 17 ++ aqt/fields.py | 10 + aqt/main.py | 94 ++++++++- aqt/modelchooser.py | 18 +- aqt/models.py | 20 ++ aqt/preferences.py | 5 +- aqt/profiles.py | 22 ++ aqt/reviewer.py | 6 +- aqt/studydeck.py | 17 ++ aqt/sync.py | 39 +++- aqt/utils.py | 25 ++- aqt/webview.py | 28 ++- documentation/hooks.md | 10 +- 48 files changed, 2832 insertions(+), 281 deletions(-) mode change 100644 => 100755 anki/template/LICENSE mode change 100644 => 100755 anki/template/README.anki diff --git a/anki/cards.py b/anki/cards.py index 3342fa9dd4f..22fe41431f2 100644 --- a/anki/cards.py +++ b/anki/cards.py @@ -11,17 +11,68 @@ # Cards ########################################################################## -# Type: 0=new, 1=learning, 2=due -# Queue: same as above, and: -# -1=suspended, -2=user buried, -3=sched buried -# Due is used differently for different queues. -# - new queue: note id or random int -# - rev queue: integer day -# - lrn queue: integer timestamp - class Card: + """ + Cards are what you review. + There can be multiple cards for each note, as determined by the Template. + + id -- the epoch milliseconds of when the card was created + nid -- The card's note's id + did -- The card's deck id + ord -- ordinal : identifies which of the card templates it corresponds to + valid values are from 0 to num templates - 1 + mod -- modificaton time as epoch seconds + usn -- update sequence number : see README.synchronization + type -- -- 0=new, 1=learning, 2=due, 3=filtered. Only for cards + queue -- + -- QUEUE_SCHED_BURIED: card buried by scheduler, -3 + -- QUEUE_USER_BURIED: card buried by user, -2 + -- QUEUE_SUSPENDED: Card suspended, -1 + -- QUEUE_NEW_CRAM: new card, 0 + -- QUEUE_LRN: cards in learning, which should be seen again today, 1 + -- QUEUE_REV: cards already learned, 2 + -- QUEUE_DAY_LRN: cards in learning but won't be seen today again, 3 + -- QUEUE_PREVIEW: cards for the preview mode, sched 2 :4 + due -- Due is used differently for different card types: + -- new: note id or random int. + Allow to select in which order new cards are seens. + -- due: integer day in which the card is due for the next time, + relative to the collection's creation time + -- learning: integer timestamp of when the next review is due + ivl -- interval (used in SRS algorithm). Negative = seconds, positive = days + factor -- easyness factor (used in SRS algorithm) + reps -- number of reviews to do + lapses -- the number of times the card went from a "was answered correctly" + -- to "was answered incorrectly" state + left + -- of the form a*1000+b, with: + -- b the number of reps left till graduation + -- a the number of reps left today + odue -- original due: only used when the card is currently in filtered deck + odid -- original did: only used when the card is currently in filtered deck + flags -- an integer. This integer mod 8 represents a "flag", which can be see in browser and while reviewing a note. Red 1, Orange 2, Green 3, Blue 4, no flag: 0. This integer divided by 8 represents currently nothing + data -- currently unused + + Values not in the database: + col -- its collection + timerStarted -- The time at which the timer started + _qa -- the dictionnary whose element q and a are questions and answers html + _note -- the note object of the card + wasNew -- + """ + def __init__(self, col, id=None): + """ + This function returns a card object from the collection given in argument. + + If an id is given, then the card is the one with this id from the collection. + Otherwise a new card, assumed to be from this collection, is created. + + Keyword arguments: + col -- a collection + id -- an identifier of a card. Int. + """ self.col = col self.timerStarted = None self._qa = None @@ -47,6 +98,10 @@ def __init__(self, col, id=None): self.data = "" def load(self): + """ + Given a card, complete it with the information extracted from the database. + + It is assumed that the card's id and col are already known.""" (self.id, self.nid, self.did, @@ -70,6 +125,10 @@ def load(self): self._note = None def flush(self): + """Insert the card into the database. + + If the cards is already in the database, it is replaced, + otherwise it is created.""" self.mod = intTime() self.usn = self.col.usn() # bug check @@ -101,6 +160,10 @@ def flush(self): self.col.log(self) def flushSched(self): + """Update the card into the database. + + This card is supposed to already + exists in the db.""" self.mod = intTime() self.usn = self.col.usn() # bug checks @@ -111,23 +174,52 @@ def flushSched(self): """update cards set mod=?, usn=?, type=?, queue=?, due=?, ivl=?, factor=?, reps=?, lapses=?, left=?, odue=?, odid=?, did=? where id = ?""", - self.mod, self.usn, self.type, self.queue, self.due, self.ivl, - self.factor, self.reps, self.lapses, - self.left, self.odue, self.odid, self.did, self.id) + self.mod, + self.usn, + self.type, + self.queue, + self.due, + self.ivl, + self.factor, + self.reps, + self.lapses, + self.left, + self.odue, + self.odid, + self.did, + self.id) self.col.log(self) def q(self, reload=False, browser=False): + """The card question with its css. + + Keyword arguments: + reload -- whether the card should be reloaded even if it is already known + browser -- whether its called from the browser (in which case the format strings are + bqfmt and not qfmt) +""" return self.css() + self._getQA(reload, browser)['q'] def a(self): + """Return the card answer with its css""" return self.css() + self._getQA()['a'] def css(self): + """Return the css of the card's model, as html code""" return "" % self.model()['css'] def _getQA(self, reload=False, browser=False): + """The QA dictionnary of the card. This dictionnary is added to the card. + + If the QA dictionnary already exists and reload is not set to true, it is directly returned. + + Keyword arguments: + browser -- ???TODO + """ if not self._qa or reload: - f = self.note(reload); m = self.model(); t = self.template() + f = self.note(reload) + m = self.model() + t = self.template() data = [self.id, f.id, m['id'], self.odid or self.did, self.ord, f.stringTags(), f.joinedFields(), self.flags] if browser: @@ -138,29 +230,44 @@ def _getQA(self, reload=False, browser=False): return self._qa def note(self, reload=False): + """The note object of the card. + + If the cards already knows its object, and reload is not true, + this object is used. + """ if not self._note or reload: self._note = self.col.getNote(self.nid) return self._note def model(self): + """The card's note's model (note type) object. This object is + described in models.py.""" return self.col.models.get(self.note().mid) def template(self): + """The card's template object. See models.py for a comment of this + object.""" m = self.model() if m['type'] == MODEL_STD: return self.model()['tmpls'][self.ord] - else: + else: #In case of cloze return self.model()['tmpls'][0] def startTimer(self): + """Start the timer of the card""" self.timerStarted = time.time() def timeLimit(self): - "Time limit for answering in milliseconds." + """Time limit for answering in milliseconds. + + According to the deck's information.""" conf = self.col.decks.confForDid(self.odid or self.did) return conf['maxTaken']*1000 def shouldShowTimer(self): + """Whether timer should be shown. + + According to the deck's information.""" conf = self.col.decks.confForDid(self.odid or self.did) return conf['timer'] @@ -170,6 +277,7 @@ def timeTaken(self): return min(total, self.timeLimit()) def isEmpty(self): + """TODO""" ords = self.col.models.availOrds( self.model(), joinFields(self.note().fields)) if self.ord not in ords: diff --git a/anki/collection.py b/anki/collection.py index fff48b4e57c..cc0f7b94c49 100644 --- a/anki/collection.py +++ b/anki/collection.py @@ -51,7 +51,90 @@ # this is initialized by storage.Collection class _Collection: - + """A collection is, basically, everything that composed an account in + Anki. + + This object is usually denoted col + + _lastSave -- time of the last save. Initially time of creation. + _undo -- An undo object. See below + + The collection is an object composed of: + usn -- USN of the collection + id -- arbitrary number since there is only one row + crt -- timestamp of the creation date. It's correct up to the day. For V1 scheduler, the hour corresponds to starting a newday. + mod -- last modified in milliseconds + scm -- schema mod time: time when "schema" was modified. + -- If server scm is different from the client scm a full-sync is required + ver -- version + dty -- dirty: unused, set to 0 + usn -- update sequence number: used for finding diffs when syncing. + -- See usn in cards table for more details. + ls -- "last sync time" + conf -- json object containing configuration options that are synced + """ + + """ + In the db, not in col objects: json array of json objects containing the models (aka Note types) + decks -- The deck manager + -- in the db it is a json array of json objects containing the deck + dconf -- json array of json objects containing the deck options + tags -- a cache of tags used in the collection (probably for autocomplete etc) + """ + + """ + conf -- ("conf" in the database.) + "curDeck": "The id (as int) of the last deck selectionned (review, adding card, changing the deck of a card)", + "activeDecks": "The list containing the current deck id and its descendent (as ints)", + "newSpread": "In which order to view to review the cards. This can be selected in Preferences>Basic. Possible values are: + 0 -- NEW_CARDS_DISTRIBUTE (Mix new cards and reviews) + 1 -- NEW_CARDS_LAST (see new cards after review) + 2 -- NEW_CARDS_FIRST (See new card before review)", + "collapseTime": "'Preferences>Basic>Learn ahead limit'*60. + If there are no other card to review, then we can review cards in learning in advance if they are due in less than this number of seconds.", + "timeLim": "'Preferences>Basic>Timebox time limit'*60. Each time this number of second elapse, anki tell you how many card you reviewed.", + "estTimes": "'Preferences>Basic>Show next review time above answer buttons'. A Boolean." + "dueCounts": "'Preferences>Basic>Show remaining card count during review'. A Boolean." + "curModel": "Id (as string) of the last note type (a.k.a. model) used (i.e. either when creating a note, or changing the note type of a note).", + "nextPos": "This is the highest value of a due value of a new card. It allows to decide the due number to give to the next note created. (This is useful to ensure that cards are seen in order in which they are added.", + "sortType": "A string representing how the browser must be sorted. Its value should be one of the possible value of 'aqt.browsers.DataModel.activeCols' (or equivalently of 'activeCols' but not any of ('question', 'answer', 'template', 'deck', 'note', 'noteTags')", + "sortBackwards": "A Boolean stating whether the browser sorting must be in increasing or decreasing order", + "addToCur": "A Boolean. True for 'When adding, default to current deck' in Preferences>Basic. False for 'Change deck depending on note type'.", + "dayLearnFirst": "A Boolean. It corresponds to the option 'Show learning cards with larger steps before reviews'. But this option does not seems to appear in the preference box", + "newBury": "A Boolean. Always set to true and not read anywhere in the code but at the place where it is set to True if it is not already true. Hence probably quite useful.", + + "lastUnburied":"The date of the last time the scheduler was initialized or reset. If it's not today, then buried notes must be unburied. This is not in the json until scheduler is used once.", + "activeCols":"the list of name of columns to show in the browser. Possible values are listed in aqt.browser.Browser.setupColumns. They are: + 'question' -- the browser column'Question', + 'answer' -- the browser column'Answer', + 'template' -- the browser column'Card', + 'deck' -- the browser column'Deck', + 'noteFld' -- the browser column'Sort Field', + 'noteCrt' -- the browser column'Created', + 'noteMod' -- the browser column'Edited', + 'cardMod' -- the browser column'Changed', + 'cardDue' -- the browser column'Due', + 'cardIvl' -- the browser column'Interval', + 'cardEase' -- the browser column'Ease', + 'cardReps' -- the browser column'Reviews', + 'cardLapses' -- the browser column'Lapses', + 'noteTags' -- the browser column'Tags', + 'note' -- the browser column'Note', + The default columns are: noteFld, template, cardDue and deck + This is not in the json at creaton. It's added when the browser is open. + " + """ + + """An undo object is of the form + [type, undoName, data] + Here, type is 1 for review, 2 for checkpoint. + undoName is the name of the action to undo. Used in the edit menu, + and in tooltip stating that undo was done. + + server -- Whether to pretend to be the server. Only set to true during anki.sync.Syncer.remove; i.e. while removing what the server says to remove. When set to true: + * the usn returned by self.usn is self._usn, otherwise -1. + * media manager does not connect nor close database connexion (I've no idea why) + """ def __init__(self, db, server=False, log=False): self._debugLog = log self.db = db @@ -163,7 +246,11 @@ def flush(self, mod=None): self._usn, self.ls, json.dumps(self.conf)) def save(self, name=None, mod=None): - "Flush, commit DB, and take out another write lock." + """ + Flush, commit DB, and take out another write lock. + + name -- + """ # let the managers conditionally flush self.models.flush() self.decks.flush() @@ -184,13 +271,15 @@ def autosave(self): return True def lock(self): - # make sure we don't accidentally bump mod time - mod = self.db.mod + """TODO. """ + mod = self.db.mod# make sure we don't accidentally bump mod time self.db.execute("update col set mod=mod") self.db.mod = mod def close(self, save=True): - "Disconnect from DB." + """Save or rollback collection's db according to save. + Close collection's db, media's db and log. + """ if self.db: if save: self.save() @@ -219,9 +308,19 @@ def rollback(self): self.lock() def modSchema(self, check): - "Mark schema modified. Call this first so user can abort if necessary." + """Mark schema modified. Call this first so user can abort if necessary. + + Raise AnkiError("abortSchemaMod") if the change is + rejected by the filter (e.g. if the user states to abort). + + Once the change is accepted, the filter is not run until a + synchronization occurs. + + Change the scm value + """ if not self.schemaChanged(): if check and not runFilter("modSchema", True): + #default hook is added in aqt/main setupHooks. It is function onSchemaMod from class AnkiQt aqt/main raise AnkiError("abortSchemaMod") self.scm = intTime(1000) self.setMod() @@ -231,10 +330,25 @@ def schemaChanged(self): return self.scm > self.ls def usn(self): + """Return the synchronization number to use. Usually, -1, since + no actions are synchronized. The exception being actions + requested by synchronization itself, when self.server is + true. In which case _usn number. + + """ return self._usn if self.server else -1 def beforeUpload(self): - "Called before a full upload." + """Called before a full upload. + + * change usn -1 to 0 in notes, card and revlog, and all models, tags, decks, deck options. + * empty graves. + * Update usn + * set modSchema to true (no nead for new upload) + * update last sync time to current schema + * Save or rollback collection's db according to save. + * Close collection's db, media's db and log. + """ tbls = "notes", "cards", "revlog" for t in tbls: self.db.execute("update %s set usn=0 where usn=-1" % t) @@ -256,15 +370,20 @@ def beforeUpload(self): ########################################################################## def getCard(self, id): + """The card object whose id is id.""" return anki.cards.Card(self, id) def getNote(self, id): + """The note object whose id is id.""" return anki.notes.Note(self, id=id) # Utils ########################################################################## def nextID(self, type, inc=True): + """Get the id next{Type} in the collection's configuration. Increment this id. + + Use 1 instead if this id does not exists in the collection.""" type = "next"+type.capitalize() id = self.conf.get(type, 1) if inc: @@ -272,7 +391,7 @@ def nextID(self, type, inc=True): return id def reset(self): - "Rebuild the queue and reload data after DB modified." + """See sched's reset documentation""" self.sched.reset() # Deletion logging @@ -309,6 +428,7 @@ def addNote(self, note): return ncards def remNotes(self, ids): + """Removes all cards associated to the notes whose id is in ids""" self.remCards(self.db.list("select id from cards where nid in "+ ids2str(ids))) @@ -327,12 +447,14 @@ def _remNotes(self, ids): ########################################################################## def findTemplates(self, note): - "Return (active), non-empty templates." + "Return non-empty templates." model = note.model() avail = self.models.availOrds(model, joinFields(note.fields)) return self._tmplsFromOrds(model, avail) def _tmplsFromOrds(self, model, avail): + """Given a list of ordinals, returns a list of templates + corresponding to those position/cloze""" ok = [] if model['type'] == MODEL_STD: for t in model['tmpls']: @@ -347,12 +469,15 @@ def _tmplsFromOrds(self, model, avail): return ok def genCards(self, nids): - "Generate cards for non-empty templates, return ids to remove." + """Ids of cards which needs to be removed. + + Generate missing cards of a note with id in nids. + """ # build map of (nid,ord) so we don't create dupes snids = ids2str(nids) - have = {} - dids = {} - dues = {} + have = {}#Associated to each nid a dictionnary from card's order to card id. + dids = {}#Associate to each nid the only deck id containing its cards. Or None if there are multiple decks + dues = {}#Associate to each nid the due value of the last card seen. for id, nid, ord, did, due, odue, odid in self.db.execute( "select id, nid, ord, did, due, odue, odid from cards where nid in "+snids): # existing cards @@ -377,10 +502,10 @@ def genCards(self, nids): if nid not in dues: dues[nid] = due # build cards for each note - data = [] + data = []#Tuples for cards to create. Each tuple is newCid, nid, did, ord, now, usn, due ts = maxID(self.db) now = intTime() - rem = [] + rem = []#cards to remove usn = self.usn() for nid, mid, flds in self.db.execute( "select id, mid, flds from notes where id in "+snids): @@ -415,10 +540,14 @@ def genCards(self, nids): data) return rem - # type 0 - when previewing in add dialog, only non-empty - # type 1 - when previewing edit, only existing - # type 2 - when previewing in models dialog, all templates def previewCards(self, note, type=0, did=None): + """Returns a list of new cards, one by template. Those cards are not flushed, and their due is always 1. + + type 0 - when previewing in add dialog, only non-empty. Seems to be used only in tests. + type 1 - when previewing edit, only existing. Seems to be used only in tests. + type 2 - when previewing in models dialog (i.e. note type modifier), return the list of cards for every single template of the model. + """ + #cms is the list of templates to consider if type == 0: cms = self.findTemplates(note) elif type == 1: @@ -433,7 +562,18 @@ def previewCards(self, note, type=0, did=None): return cards def _newCard(self, note, template, due, flush=True, did=None): - "Create a new card." + """A new card object belonging to this collection. + Its nid according to note, + ord according to template + did according to template, or to model, or default if otherwise deck is dynamic + Cards is flushed or not according to flush parameter + + keyword arguments: + note -- the note of this card + template -- the template of this card + due -- The due time of this card, assuming no random + flush -- whether this card should be push in the db + """ card = anki.cards.Card(self) card.nid = note.id card.ord = template['ord'] @@ -459,6 +599,14 @@ def _newCard(self, note, template, due, flush=True, did=None): return card def _dueForDid(self, did, due): + """The due date of a card. Itself if not random mode. A random number + depending only on the due date otherwise. + + keyword arguments + did -- the deck id of the considered card + due -- the due time of the considered card + + """ conf = self.decks.confForDid(did) # in order due? if conf['new']['order'] == NEW_CARDS_DUE: @@ -474,13 +622,17 @@ def _dueForDid(self, did, due): ########################################################################## def isEmpty(self): + """Is there no cards in this collection.""" return not self.db.scalar("select 1 from cards limit 1") def cardCount(self): return self.db.scalar("select count() from cards") def remCards(self, ids, notes=True): - "Bulk delete cards by ID." + """Bulk delete cards by ID. + + keyword arguments: + notes -- whether note without cards should be deleted.""" if not ids: return sids = ids2str(ids) @@ -497,6 +649,7 @@ def remCards(self, ids, notes=True): self._remNotes(nids) def emptyCids(self): + """The card id of empty cards of the collection""" rem = [] for m in self.models.all(): rem += self.genCards(self.models.nids(m)) @@ -519,7 +672,7 @@ def _fieldData(self, snids): "select id, mid, flds from notes where id in "+snids) def updateFieldCache(self, nids): - "Update field checksums and sort cache, after find&replace, etc." + "Update field checksums and sort cache, after find&replace, changing model, etc." snids = ids2str(nids) r = [] for (nid, mid, flds) in self._fieldData(snids): @@ -539,6 +692,13 @@ def updateFieldCache(self, nids): def renderQA(self, ids=None, type="card"): # gather metadata + """TODO + + The list of renderQA for each cards whose type belongs to ids. + + Types may be card(default), note, model or all (in this case, ids is not used). + It seems to be called nowhere + """ if type == "card": where = "and c.id in " + ids2str(ids) elif type == "note": @@ -553,53 +713,82 @@ def renderQA(self, ids=None, type="card"): for row in self._qaData(where)] def _renderQA(self, data, qfmt=None, afmt=None): - "Returns hash of id, question, answer." - # data is [cid, nid, mid, did, ord, tags, flds, cardFlags] - # unpack fields and create dict - flist = splitFields(data[6]) - fields = {} - model = self.models.get(data[2]) - for (name, (idx, conf)) in list(self.models.fieldMap(model).items()): + """Returns hash of id, question, answer. + + Keyword arguments: + data -- [cid, nid, mid, did, ord, tags, flds] (see db + documentation for more information about those values) + This corresponds to the information you can obtain in templates, using {{Tags}}, {{Type}}, etc.. + qfmt -- question format string (as in template) + afmt -- answer format string (as in template) + + unpack fields and create dict + TODO comment better + + """ + cid, nid, mid, did, ord, tags, flds, cardFlags = data + flist = splitFields(flds)#the list of fields + fields = {} # + #name -> ord for each field, tags + # Type: the name of the model, + # Deck, Subdeck: their name + # Card: the template name + # cn: 1 for n being the ord+1 + # FrontSide : + model = self.models.get(mid) + for (name, (idx, conf)) in list(self.models.fieldMap(model).items()):#conf is not used fields[name] = flist[idx] - fields['Tags'] = data[5].strip() + fields['Tags'] = tags.strip() fields['Type'] = model['name'] - fields['Deck'] = self.decks.name(data[3]) + fields['Deck'] = self.decks.name(did) fields['Subdeck'] = fields['Deck'].split('::')[-1] - fields['CardFlag'] = self._flagNameFromCardFlags(data[7]) - if model['type'] == MODEL_STD: - template = model['tmpls'][data[4]] - else: + fields['CardFlag'] = self._flagNameFromCardFlags(cardFlags) + if model['type'] == MODEL_STD:#model['type'] is distinct from fields['Type'] + template = model['tmpls'][ord] + else:#for cloze deletions template = model['tmpls'][0] fields['Card'] = template['name'] - fields['c%d' % (data[4]+1)] = "1" + fields['c%d' % (ord+1)] = "1" # render q & a - d = dict(id=data[0]) + d = dict(id=cid) + # id: card id qfmt = qfmt or template['qfmt'] afmt = afmt or template['afmt'] for (type, format) in (("q", qfmt), ("a", afmt)): - if type == "q": - format = re.sub("{{(?!type:)(.*?)cloze:", r"{{\1cq-%d:" % (data[4]+1), format) + if type == "q":#if/else is in the loop in order for d['q'] to be defined below + format = re.sub("{{(?!type:)(.*?)cloze:", r"{{\1cq-%d:" % (ord+1), format) + #Replace {{'foo'cloze: by {{'foo'cq-(ord+1), where 'foo' does not begins with "type:" format = format.replace("<%cloze:", "<%%cq:%d:" % ( - data[4]+1)) + ord+1)) + #Replace <%cloze: by <%%cq:(ord+1) else: - format = re.sub("{{(.*?)cloze:", r"{{\1ca-%d:" % (data[4]+1), format) + format = re.sub("{{(.*?)cloze:", r"{{\1ca-%d:" % (ord+1), format) + #Replace {{'foo'cloze: by {{'foo'ca-(ord+1) format = format.replace("<%cloze:", "<%%ca:%d:" % ( - data[4]+1)) + ord+1)) + #Replace <%cloze: by <%%ca:(ord+1) fields['FrontSide'] = stripSounds(d['q']) - fields = runFilter("mungeFields", fields, model, data, self) - html = anki.template.render(format, fields) + #d['q'] is defined during loop's first iteration + fields = runFilter("mungeFields", fields, model, data, self) # TODO check + html = anki.template.render(format, fields) #replace everything of the form {{ by its value TODO check d[type] = runFilter( - "mungeQA", html, type, fields, model, data, self) + "mungeQA", html, type, fields, model, data, self) # TODO check # empty cloze? if type == 'q' and model['type'] == MODEL_CLOZE: - if not self.models._availClozeOrds(model, data[6], False): + if not self.models._availClozeOrds(model, flds, False): d['q'] += ("

" + _( "Please edit this note and add some cloze deletions. (%s)") % ( "%s" % (HELP_SITE, _("help")))) + #in the case where there is a cloze note type + #without {{cn in fields indicated by + #{{cloze:fieldName; an error message should be + #shown return d def _qaData(self, where=""): - "Return [cid, nid, mid, did, ord, tags, flds, cardFlags] db query" + """The list of [cid, nid, mid, did, ord, tags, flds, cardFlags] for each pair cards satisfying where. + + Where should start with an and.""" return self.db.execute(""" select c.id, f.id, f.mid, c.did, c.ord, f.tags, f.flds, c.flags from cards c, notes f @@ -619,6 +808,7 @@ def findCards(self, query, order=False): return anki.find.Finder(self).findCards(query, order) def findNotes(self, query): + "Return a list of notes ids for QUERY." return anki.find.Finder(self).findNotes(query) def findReplace(self, nids, src, dst, regex=None, field=None, fold=True): @@ -656,19 +846,27 @@ def timeboxReached(self): # Undo ########################################################################## + # [type, undoName, data] + # type 1 = review; type 2 = checkpoint def clearUndo(self): - # [type, undoName, data] - # type 1 = review; type 2 = checkpoint + """Erase all undo information from the collection.""" self._undo = None def undoName(self): - "Undo menu item name, or None if undo unavailable." + """The name of the action which could potentially be undone. + + None if nothing can be undone. This let test whether something + can be undone. + """ if not self._undo: return None return self._undo[1] def undo(self): + """Undo the last operation. + + Assuming an undo object exists.""" if self._undo[0] == 1: return self._undoReview() else: @@ -680,7 +878,7 @@ def markReview(self, card): if self._undo[0] == 1: old = self._undo[2] self.clearUndo() - wasLeech = card.note().hasTag("leech") or False + wasLeech = card.note().hasTag("leech") or False#The or is probably useless. self._undo = [1, _("Review"), old + [copy.copy(card)], wasLeech] def _undoReview(self): @@ -728,7 +926,17 @@ def _undoOp(self): ########################################################################## def basicCheck(self): - "Basic integrity check for syncing. True if ok." + """True if basic integrity is meet. + + Used before and after sync, or before a full upload. + + Tests: + * whether each card belong to a note + * each note has a model + * each note has a card + * each card's ord is valid according to the note model. +ooo + """ # cards without notes if self.db.scalar(""" select 1 from cards where nid not in (select id from notes) limit 1"""): @@ -757,8 +965,11 @@ def fixIntegrity(self): curs = self.db.cursor() self.save() oldSize = os.stat(self.path)[stat.ST_SIZE] + + # whether sqlite find a problem in its database if self.db.scalar("pragma integrity_check") != "ok": return (_("Collection is corrupt. Please see the manual."), False) + # note types with a missing model ids = self.db.list(""" select id from notes where mid not in """ + ids2str(self.models.ids())) @@ -768,6 +979,7 @@ def fixIntegrity(self): "Deleted %d notes with missing note type.", len(ids)) % len(ids)) self.remNotes(ids) + # for each model for m in self.models.all(): for t in m['tmpls']: @@ -882,6 +1094,7 @@ def fixIntegrity(self): newSize = os.stat(self.path)[stat.ST_SIZE] txt = _("Database rebuilt and optimized.") ok = not problems + print("Adding in collection.py") problems.append(txt) # if any problems were found, force a full sync if not ok: @@ -890,6 +1103,7 @@ def fixIntegrity(self): return ("\n".join(problems), ok) def optimize(self): + """Tell sqlite to optimize the db""" self.db.setAutocommit(True) self.db.execute("vacuum") self.db.execute("analyze") @@ -900,6 +1114,15 @@ def optimize(self): ########################################################################## def log(self, *args, **kwargs): + """Generate the string [time] path:fn(): args list + + if args is not string, it is represented using pprint.pformat + + if self._debugLog is True, it is hadded to _logHnd + if devMode is True, this string is printed + + TODO look traceback/extract stack and fn + """ if not self._debugLog: return def customRepr(x): @@ -908,7 +1131,8 @@ def customRepr(x): return pprint.pformat(x) path, num, fn, y = traceback.extract_stack( limit=2+kwargs.get("stack", 0))[0] - buf = "[%s] %s:%s(): %s" % (intTime(), os.path.basename(path), fn, + time = datetime.datetime.now() + buf = "[%s] %s:%s(): %s" % (time, os.path.basename(path), fn, ", ".join([customRepr(x) for x in args])) self._logHnd.write(buf + "\n") if devMode: diff --git a/anki/consts.py b/anki/consts.py index d813447196e..3ad297b7f1d 100644 --- a/anki/consts.py +++ b/anki/consts.py @@ -54,6 +54,7 @@ HELP_SITE="http://ankisrs.net/docs/manual.html" +### NEW CONSTANTS # Queue types QUEUE_SCHED_BURIED = -3 QUEUE_USER_BURIED = -2 @@ -85,6 +86,7 @@ BUTTON_TWO = 2 BUTTON_THREE = 3 BUTTON_FOUR = 4 +### Ends of NEW CONSTANTS # Labels ########################################################################## diff --git a/anki/db.py b/anki/db.py index 7209a595b3d..968e603060e 100644 --- a/anki/db.py +++ b/anki/db.py @@ -18,6 +18,12 @@ def __init__(self, path, timeout=0): self.mod = False def execute(self, sql, *a, **ka): + """The result of execute on the database with sql query and either ka if it exists, or a. + + If insert, update or delete, mod is set to True + If self.echo, prints the execution time + if self.echo is "2", also print the arguments. + """ s = sql.strip().lower() # mark modified? for stmt in "insert", "update", "delete": @@ -38,6 +44,11 @@ def execute(self, sql, *a, **ka): return res def executemany(self, sql, l): + """The result of executmany on the database with sql query and l list. + + Mod is set to True + If self.echo, prints the execution time + """ self.mod = True t = time.time() self._db.executemany(sql, l) @@ -47,39 +58,50 @@ def executemany(self, sql, l): print(l) def commit(self): + """Commit database. + If self.echo, prints the execution time.""" t = time.time() self._db.commit() if self.echo: print("commit %0.3fms" % ((time.time() - t)*1000)) def executescript(self, sql): + """executescript with sql on the database. + If self.echo, prints sql + set mod to True.""" self.mod = True if self.echo: print(sql) self._db.executescript(sql) def rollback(self): + """rollback on the db""" self._db.rollback() def scalar(self, *a, **kw): + """The first value of the first tuple of the result, if it exists. None otherwise.""" res = self.execute(*a, **kw).fetchone() if res: return res[0] return None def all(self, *a, **kw): + """The list of rows of the answer.""" return self.execute(*a, **kw).fetchall() def first(self, *a, **kw): + """The first row of the answer.""" c = self.execute(*a, **kw) res = c.fetchone() c.close() return res def list(self, *a, **kw): + """The list of first elements of tuples of the answer.""" return [x[0] for x in self.execute(*a, **kw)] def close(self): + """Close the underlying database.""" self._db.text_factory = None self._db.close() diff --git a/anki/decks.py b/anki/decks.py index 2165db7f81a..f7b679d39c1 100644 --- a/anki/decks.py +++ b/anki/decks.py @@ -2,11 +2,97 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +"""This module deals with decks and their configurations. + +self.decks is the dictionnary associating an id to the deck with this id +self.dconf is the dictionnary associating an id to the dconf with this id + +A deck is a dict composed of: +new/rev/lrnToday -- two number array. + First one is currently unused + The second one is equal to the number of cards seen today in this deck minus the number of new cards in custom study today. + BEWARE, it's changed in anki.sched(v2).Scheduler._updateStats and anki.sched(v2).Scheduler._updateCutoff.update but can't be found by grepping 'newToday', because it's instead written as type+"Today" with type which may be new/rev/lrnToday +timeToday -- two number array used somehow for custom study, seems to be currently unused +conf -- (string) id of option group from dconf, or absent in dynamic decks +usn -- Update sequence number: used in same way as other usn vales in db +desc -- deck description, it is shown when cards are learned or reviewd +dyn -- 1 if dynamic (AKA filtered) deck, +collapsed -- true when deck is collapsed, +extendNew -- extended new card limit (for custom study). Potentially absent, only used in aqt/customstudy.py. By default 10 +extendRev -- extended review card limit (for custom study), Potentially absent, only used in aqt/customstudy.py. By default 10. +name -- name of deck, +browserCollapsed -- true when deck collapsed in browser, +id -- deck ID (automatically generated long), +mod -- last modification time, +mid -- the model of the deck +""" + + + +"""A configuration of deck is a dictionnary composed of: +name -- its name, including the parents, and the "::" + + + + +A configuration of deck (dconf) is composed of: +name -- its name +new -- The configuration for new cards, see below. +lapse -- The configuration for lapse cards, see below. +rev -- The configuration for review cards, see below. +maxTaken -- The number of seconds after which to stop the timer +timer -- whether timer should be shown (1) or not (0) +autoplay -- whether the audio associated to a question should be +played when the question is shown +replayq -- whether the audio associated to a question should be +played when the answer is shown +mod -- Last modification time +usn -- see USN documentation +dyn -- Whether this deck is dynamic. Not present in the default configurations +id -- deck ID (automatically generated long). Not present in the default configurations. + +The configuration related to new cards is composed of: +delays -- The list of successive delay between the learning steps of +the new cards, as explained in the manual. +ints -- The delays according to the button pressed while leaving the +learning mode. +initial factor -- The initial ease factor +separate -- delay between answering Good on a card with no steps left, and seeing the card again. Seems to be unused in the code +order -- In which order new cards must be shown. NEW_CARDS_RANDOM = 0 +and NEW_CARDS_DUE = 1 +perDay -- Maximal number of new cards shown per day +bury -- Whether to bury cards related to new cards answered + +The configuration related to lapses card is composed of: +delays -- The delays between each relearning while the card is lapsed, +as in the manual +mult -- percent by which to multiply the current interval when a card +goes has lapsed +minInt -- a lower limit to the new interval after a leech +leechFails -- the number of lapses authorized before doing leechAction +leechAction -- What to do to leech cards. 0 for suspend, 1 for +mark. Numbers according to the order in which the choices appear in +aqt/dconf.ui + + +The configuration related to review card is composed of: +perDay -- Numbers of cards to review per day +ease4 -- the number to add to the easyness when the easy button is +pressed +fuzz -- The new interval is multiplied by a random number between +-fuzz and fuzz +minSpace -- not currently used +ivlFct -- multiplication factor applied to the intervals Anki +generates +maxIvl -- the maximal interval for review +bury -- If True, when a review card is answered, the related cards of +its notes are buried +""" + import copy, operator import unicodedata -import json - -from anki.utils import intTime, ids2str +from anki.utils import intTime, ids2str, json from anki.hooks import runHook from anki.consts import * from anki.lang import _ @@ -90,14 +176,28 @@ } class DeckManager: - + """ + col -- the collection associated to this Deck manager + decks -- associating to each id (as string) its deck + dconf -- associating to each id (as string) its configuration(option) + """ # Registry save/load ############################################################# def __init__(self, col): + """State that the collection of the created object is the first argument.""" self.col = col def load(self, decks, dconf): + """Assign decks and dconf of this object using the two parameters. + + It also ensures that the number of cards per day is at most + 999999 or correct this error. + + Keyword arguments: + decks -- json dic associating to each id (as string) its deck + dconf -- json dic associating to each id (as string) its configuration(option) + """ self.decks = json.loads(decks) self.dconf = json.loads(dconf) # set limits to within bounds @@ -113,13 +213,21 @@ def load(self, decks, dconf): self.changed = False def save(self, g=None): - "Can be called with either a deck or a deck configuration." + """State that the DeckManager has been changed. Changes the + mod and usn of the potential argument. + + The potential argument can be either a deck or a deck + configuration. + """ if g: g['mod'] = intTime() g['usn'] = self.col.usn() self.changed = True def flush(self): + """Puts the decks and dconf in the db if the manager state that some + changes happenned. + """ if self.changed: self.col.db.execute("update col set decks=?, dconf=?", json.dumps(self.decks), @@ -130,7 +238,14 @@ def flush(self): ############################################################# def id(self, name, create=True, type=None): - "Add a deck with NAME. Reuse deck if already exists. Return id as int." + """Returns a deck's id with a given name. Potentially creates it. + + Keyword arguments: + name -- the name of the new deck. " are removed. + create -- States whether the deck must be created if it does + not exists. Default true, otherwise return None + type -- A deck to copy in order to create this deck + """ if type is None: type = defaultDeck name = name.replace('"', '') @@ -157,7 +272,17 @@ def id(self, name, create=True, type=None): return int(id) def rem(self, did, cardsToo=False, childrenToo=True): - "Remove the deck. If cardsToo, delete any cards inside." + """Remove the deck whose id is did. + + Does not delete the default deck, but rename it. + + Log the removal, even if the deck does not exists, assuming it + is not default. + + Keyword arguments: + cardsToo -- if set to true, delete its card. + ChildrenToo -- if set to false, + """ if str(did) == '1': # we won't allow the default deck to be deleted, but if it's a # child of an existing deck then it needs to be renamed @@ -199,42 +324,57 @@ def rem(self, did, cardsToo=False, childrenToo=True): cids = self.col.db.list( "select id from cards where did=? or odid=?", did, did) self.col.remCards(cids) - # delete the deck and add a grave + # delete the deck and add a grave (it seems no grave is added) del self.decks[str(did)] - # ensure we have an active deck + # ensure we have an active deck. if did in self.active(): self.select(int(list(self.decks.keys())[0])) self.save() def allNames(self, dyn=True): - "An unsorted list of all deck names." + """An unsorted list of all deck names. + + Keyword arguments: + dyn -- if set to false, do not list the dynamic decks. + """ if dyn: return [x['name'] for x in list(self.decks.values())] else: return [x['name'] for x in list(self.decks.values()) if not x['dyn']] def all(self): - "A list of all decks." + """A list of all deck objects.""" return list(self.decks.values()) def allIds(self): + """A list of all deck's id.""" return list(self.decks.keys()) def collapse(self, did): + """Change the collapsed state of deck whose id is did. Then + save the change.""" deck = self.get(did) deck['collapsed'] = not deck['collapsed'] self.save(deck) def collapseBrowser(self, did): + """Change the browserCollapsed state of deck whose id is did. Then + save the change.""" deck = self.get(did) collapsed = deck.get('browserCollapsed', False) deck['browserCollapsed'] = not collapsed self.save(deck) def count(self): + """The number of decks.""" return len(self.decks) def get(self, did, default=True): + """Returns the deck objects whose id is did. + + If Default, return the default deck, otherwise None. + + """ id = str(did) if id in self.decks: return self.decks[id] @@ -242,7 +382,7 @@ def get(self, did, default=True): return self.decks['1'] def byName(self, name): - "Get deck with NAME." + "Get deck object with NAME." for m in list(self.decks.values()): if m['name'] == name: return m @@ -255,10 +395,16 @@ def update(self, g): self.save() def rename(self, g, newName): - "Rename deck prefix to NAME if not exists. Updates children." + """Rename the deck object g to newName. Updates + children. Creates parents of newName if required. + + If newName already exists or if it a descendant of a filtered + deck, the operation is aborted.""" # make sure target node doesn't already exist if newName in self.allNames(): raise DeckRenameError(_("That deck already exists.")) + # ensure we have parents. + newName = self._ensureParents(newName) # make sure we're not nesting under a filtered deck for p in self.parentsByName(newName): if p['dyn']: @@ -280,14 +426,20 @@ def rename(self, g, newName): self.maybeAddToActive() def renameForDragAndDrop(self, draggedDeckDid, ontoDeckDid): + """Rename the deck whose id is draggedDeckDid as a children of + the deck whose id is ontoDeckDid.""" draggedDeck = self.get(draggedDeckDid) draggedDeckName = draggedDeck['name'] ontoDeckName = self.get(ontoDeckDid)['name'] if ontoDeckDid is None or ontoDeckDid == '': + #if the deck is dragged to toplevel if len(self._path(draggedDeckName)) > 1: + #And is not already at top level self.rename(draggedDeck, self._basename(draggedDeckName)) elif self._canDragAndDrop(draggedDeckName, ontoDeckName): + #The following three lines seems to be useless, as they + #repeat lines above draggedDeck = self.get(draggedDeckDid) draggedDeckName = draggedDeck['name'] ontoDeckName = self.get(ontoDeckDid)['name'] @@ -295,6 +447,14 @@ def renameForDragAndDrop(self, draggedDeckDid, ontoDeckDid): self.rename(draggedDeck, ontoDeckName + "::" + self._basename(draggedDeckName)) def _canDragAndDrop(self, draggedDeckName, ontoDeckName): + """Whether draggedDeckName can be moved as a children of + ontoDeckName. + + draggedDeckName should not be dragged onto a descendant of + itself (nor itself). + It should not either be dragged to its parent because the + action would be useless. + """ if draggedDeckName == ontoDeckName \ or self._isParent(ontoDeckName, draggedDeckName) \ or self._isAncestor(draggedDeckName, ontoDeckName): @@ -303,19 +463,27 @@ def _canDragAndDrop(self, draggedDeckName, ontoDeckName): return True def _isParent(self, parentDeckName, childDeckName): + """Whether childDeckName is a direct child of parentDeckName.""" return self._path(childDeckName) == self._path(parentDeckName) + [ self._basename(childDeckName) ] def _isAncestor(self, ancestorDeckName, descendantDeckName): + """Whether ancestorDeckName is an ancestor of + descendantDeckName; or itself.""" ancestorPath = self._path(ancestorDeckName) return ancestorPath == self._path(descendantDeckName)[0:len(ancestorPath)] def _path(self, name): + """Given a name, split according to ::""" return name.split("::") def _basename(self, name): + """The part of name after the last ::""" return self._path(name)[-1] def _ensureParents(self, name): - "Ensure parents exist, and return name with case matching parents." + """Ensure parents exist, and return name with case matching parents. + + Parents are created if they do not already exists. + """ s = "" path = self._path(name) if len(path) < 2: @@ -336,10 +504,15 @@ def _ensureParents(self, name): ############################################################# def allConf(self): - "A list of all deck config." + "A list of all deck config object." return list(self.dconf.values()) def confForDid(self, did): + """The dconf object of the deck whose id is did. + + If did is the id of a dynamic deck, the deck is + returned. Indeed, it has embedded conf. + """ deck = self.get(did, default=False) assert deck if 'conf' in deck: @@ -350,14 +523,20 @@ def confForDid(self, did): return deck def getConf(self, confId): + """The dconf object whose id is confId.""" return self.dconf[str(confId)] def updateConf(self, g): + """Add g to the set of dconf's. Potentially replacing a dconf with the +same id.""" self.dconf[str(g['id'])] = g self.save() def confId(self, name, cloneFrom=None): - "Create a new configuration and return id." + """Create a new configuration and return its id. + + Keyword arguments + cloneFrom -- The configuration copied by the new one.""" if cloneFrom is None: cloneFrom = defaultConf c = copy.deepcopy(cloneFrom) @@ -372,7 +551,14 @@ def confId(self, name, cloneFrom=None): return id def remConf(self, id): - "Remove a configuration and update all decks using it." + """Remove a configuration and update all decks using it. + + The new conf of the deck using this configuation is the + default one. + + Keyword arguments: + id -- The id of the configuration to remove. Should not be the + default conf.""" assert int(id) != 1 self.col.modSchema(check=True) del self.dconf[str(id)] @@ -385,10 +571,15 @@ def remConf(self, id): self.save(g) def setConf(self, grp, id): + """Takes a deck objects, switch his id to id and save it as + edited. + + Currently used in tests only.""" grp['conf'] = id self.save(grp) def didsForConf(self, conf): + """The dids of the decks using the configuration conf.""" dids = [] for deck in list(self.decks.values()): if 'conf' in deck and deck['conf'] == conf['id']: @@ -396,6 +587,11 @@ def didsForConf(self, conf): return dids def restoreToDefault(self, conf): + """Change the configuration to default. + + The only remaining part of the configuration are: the order of + new card, the name and the id. + """ oldOrder = conf['new']['order'] new = copy.deepcopy(defaultConf) new['id'] = conf['id'] @@ -410,28 +606,49 @@ def restoreToDefault(self, conf): ############################################################# def name(self, did, default=False): + """The name of the deck whose id is did. + + If no such deck exists: if default is set to true, then return + default deck's name. Otherwise return "[no deck]". + """ deck = self.get(did, default=default) if deck: return deck['name'] return _("[no deck]") def nameOrNone(self, did): + """The name of the deck whose id is did, if it exists. None + otherwise.""" deck = self.get(did, default=False) if deck: return deck['name'] return None def setDeck(self, cids, did): + """Change the deck of the cards of cids to did. + + Keyword arguments: + did -- the id of the new deck + cids -- a list of ids of cards + """ self.col.db.execute( "update cards set did=?,usn=?,mod=? where id in "+ ids2str(cids), did, self.col.usn(), intTime()) def maybeAddToActive(self): - # reselect current deck, or default if current has disappeared + """reselect current deck, or default if current has + disappeared.""" + #It seems that nothing related to default happen in this code + #nor in the function called by this code. + #maybe is not appropriate, since no condition occurs c = self.current() self.select(c['id']) def cids(self, did, children=False): + """Return the list of id of cards whose deck's id is did. + + If Children is set to true, returns also the list of the cards + of the descendant.""" if not children: return self.col.db.list("select id from cards where did=?", did) dids = [did] @@ -441,6 +658,8 @@ def cids(self, did, children=False): ids2str(dids)) def _recoverOrphans(self): + """Move the cards whose deck does not exists to the default + deck, without changing the mod date.""" dids = list(self.decks.keys()) mod = self.col.db.mod self.col.db.execute("update cards set did = 1 where did not in "+ @@ -487,14 +706,18 @@ def active(self): return self.col.conf['activeDecks'] def selected(self): - "The currently selected did." + """The did of the currently selected deck.""" return self.col.conf['curDeck'] def current(self): + """The currently selected deck object""" return self.get(self.selected()) def select(self, did): - "Select a new branch." + """Change activeDecks to the list containing did and the did + of its children. + + Also mark the manager as changed.""" # make sure arg is an int did = int(did) # current deck @@ -506,7 +729,7 @@ def select(self, did): self.changed = True def children(self, did): - "All children of did, as (name, id)." + "All descendant of did, as (name, id)." name = self.get(did)['name'] actv = [] for g in self.all(): @@ -515,6 +738,15 @@ def children(self, did): return actv def childDids(self, did, childMap): + """The list of all ancestors of did, as deck objects. + + The list starts with the toplevel ancestors of did and its + i-th element is the ancestor with i times ::. + + Keyword arguments: + did -- the id of the deck we consider + childMap -- dictionnary, associating to a deck id its node""" + # get ancestors names def gather(node, arr): for did, child in node.items(): arr.append(did) @@ -543,8 +775,15 @@ def childMap(self): return childMap def parents(self, did, nameMap=None): - "All parents of did." - # get parent and grandparent names + """The list of all ancestors of did, as deck objects. + + The list starts with the toplevel ancestors of did and its + i-th element is the ancestor with i times ::. + + Keyword arguments: + did -- the id of the deck + nameMap -- dictionnary: deck id-> Node + """ parents = [] for part in self.get(did)['name'].split("::")[:-1]: if not parents: @@ -577,6 +816,9 @@ def parentsByName(self, name): return parents def nameMap(self): + """ + Dictionnary from deck name to deck object. + """ return dict((d['name'], d) for d in self.decks.values()) # Sync handling diff --git a/anki/exporting.py b/anki/exporting.py index 913e643fc2c..9c29dec7952 100644 --- a/anki/exporting.py +++ b/anki/exporting.py @@ -11,9 +11,14 @@ from anki import Collection class Exporter: + """An abstract class. Inherited by class actually doing some kind of export. + + count -- the number of cards to export. + """ includeHTML = None def __init__(self, col, did=None): + #Currently, did is never set during initialisation. self.col = col self.did = did @@ -21,7 +26,13 @@ def doExport(self, path): raise Exception("not implemented") def exportInto(self, path): - self._escapeCount = 0 + """Export into path. + + This is the method called from the GUI to actually export things. + + Keyword arguments: + path -- a path of file in which to export""" + self._escapeCount = 0# not used ANYWHERE in the code as of 25 november 2018 file = open(path, "wb") self.doExport(file) file.close() @@ -57,6 +68,7 @@ def stripHTML(self, text): return s def cardIds(self): + """card ids of cards in deck self.did if it is set, all ids otherwise.""" if not self.did: cids = self.col.db.list("select id from cards") else: @@ -257,7 +269,7 @@ def postExport(self): # overwrite to apply customizations to the deck before it's closed, # such as update the deck description pass - + def removeSystemTags(self, tags): return self.src.tags.remFromStr("marked leech", tags) @@ -387,6 +399,7 @@ def doExport(self, z, path): ########################################################################## def exporters(): + """A list of pairs (description of an exporter class, the class)""" def id(obj): return ("%s (*%s)" % (obj.key, obj.ext), obj) exps = [ diff --git a/anki/find.py b/anki/find.py index c0080fc7d19..5b4b66cd933 100644 --- a/anki/find.py +++ b/anki/find.py @@ -15,6 +15,10 @@ ########################################################################## class Finder: + """ + col: the collection used for opening this Finder. + search: a dictionnary such that the query key:value is evaluated by self.search[key]((value,args)), with args a list of tag. This function potentially add tags (in _findTag) and return a sql part to put after the where. It may also return "skip", in which case the code is not added to sql. + """ def __init__(self, col): self.col = col @@ -53,6 +57,7 @@ def findCards(self, query, order=False): return res def findNotes(self, query): + "Return a list of notes ids for QUERY." tokens = self._tokenize(query) preds, args = self._where(tokens) if preds is None: @@ -186,6 +191,9 @@ def add(txt, wrap=True): return s['q'], args def _query(self, preds, order): + """Return the set of card ids, of card satisfying predicate preds, + where c is a card and n its note, ordered according to the sql + `order`""" # can we skip the note table? if "n." not in preds and "n." not in order: sql = "select c.id from cards c where " @@ -242,6 +250,10 @@ def _order(self, order): ###################################################################### def _findTag(self, args): + """A sql query as in 'tag:val'. Add the tag val to args. Returns a + query which, given a tag, search it. + + """ (val, args) = args if val == "none": return 'n.tags = ""' @@ -254,6 +266,7 @@ def _findTag(self, args): return "n.tags like ? escape '\\'" def _findCardState(self, args): + """A sql query, as in 'is:foo'""" (val, args) = args if val in ("review", "new", "learn"): if val == "review": @@ -274,6 +287,10 @@ def _findCardState(self, args): self.col.sched.today, self.col.sched.dayCutoff) def _findFlag(self, args): + """A sql query restricting cards to the one whose flag is `val`, as in + 'flag:val' + + """ (val, args) = args if not val or val not in "01234": return @@ -282,6 +299,13 @@ def _findFlag(self, args): return "(c.flags & %d) == %d" % (mask, val) def _findRated(self, args): + """A sql query restricting cards as in 'rated:val', where val is of + the form numberOfDay or rated:numberOfDay:ease. + + I.e. last review is at most `numberOfDay` days ago, and the button + pressed was `ease`. + + """ # days(:optional_ease) (val, args) = args r = val.split(":") @@ -301,6 +325,8 @@ def _findRated(self, args): (cutoff, ease)) def _findAdded(self, args): + """A sql query as in 'added:val', it restricts cards to ones which + were added at most val days ago.""" (val, args) = args try: days = int(val) @@ -347,24 +373,44 @@ def _findText(self, val, args): return "(n.sfld like ? escape '\\' or n.flds like ? escape '\\')" def _findNids(self, args): + """A sql query restricting to notes whose id is in the list + `val`. `val` should contains only numbers and commas. It + corresponds to the query `nid:val`. + + """ (val, args) = args if re.search("[^0-9,]", val): return return "n.id in (%s)" % val def _findCids(self, args): + """A sql query restricting to cards whose id is in the list + `val`. `val` should contains only numbers and commas. It + corresponds to the query `cid:val`. + + """ (val, args) = args if re.search("[^0-9,]", val): return return "c.id in (%s)" % val def _findMid(self, args): + """A sql query restricting model (i.e. note type) to whose id is in + the list `val`. `val` should contains only numbers and + commas. It corresponds to the query `mid:val`. + + """ (val, args) = args if re.search("[^0-9]", val): return return "n.mid = %s" % val def _findModel(self, args): + """A sql query restricting model (i.e. note type) to whose name is in + the list `val`. `val` should contains only numbers and + commas. It corresponds to the query `mid:val`. + + """ (val, args) = args ids = [] val = val.lower() @@ -429,6 +475,11 @@ def _findTemplate(self, args): return " or ".join(lims) def _findField(self, field, val): + """A sql query restricting the notes to the ones having `val` in the field `field`. + + Same than "field:val". Field is assumed not to be one of the keyword of the browser. + + """ field = field.lower() val = val.replace("*", "%") # find models that have that field diff --git a/anki/hooks.py b/anki/hooks.py index bbd9a25f0b8..0308132e9a6 100644 --- a/anki/hooks.py +++ b/anki/hooks.py @@ -21,7 +21,11 @@ _hooks = {} def runHook(hook, *args): - "Run all functions on hook." + """Run all functions on hook. Do not return value. + + keyword arguments: + hook -- a hook name (string) + *args -- the list of arguments to give to the functions""" hook = _hooks.get(hook, None) if hook: for func in hook: @@ -32,6 +36,14 @@ def runHook(hook, *args): raise def runFilter(hook, arg, *args): + """Apply each function on hook to the result of the last function + and *args. The first argument is arg. Return the value returned by + the last function. + + keyword arguments: + hook -- a hook name (string) + arg -- the arg, which is modified by each method + *args -- the list of arguments given to every functions""" hook = _hooks.get(hook, None) if hook: for func in hook: @@ -59,18 +71,32 @@ def remHook(hook, func): ############################################################################## def wrap(old, new, pos="after"): - "Override an existing function." + """Override an existing function. + TODO + + keyword arguments: + old -- The function which is overrided + new -- The function which should be added + """ def repl(*args, **kwargs): + """If pos is after (default), execute old and return new. + If pos is "before", execute new and return pold. + otherwise, execute new, with parameter _old=old. + + In each case, gives to each called function the parameters + *args and **kwargs. + """ if pos == "after": old(*args, **kwargs) return new(*args, **kwargs) elif pos == "before": new(*args, **kwargs) return old(*args, **kwargs) - else: + else:# around return new(_old=old, *args, **kwargs) def decorator_wrapper(f, *args, **kwargs): + """Ignore the first argument, similar to repl.""" return repl(*args, **kwargs) return decorator.decorator(decorator_wrapper)(old) diff --git a/anki/importing/__init__.py b/anki/importing/__init__.py index ab390836c73..2801f37d55b 100644 --- a/anki/importing/__init__.py +++ b/anki/importing/__init__.py @@ -10,6 +10,7 @@ from anki.importing.pauker import PaukerImporter from anki.lang import _ +"""Used by the file system window, to ensure that it knows which kind of file to search for. And then which importer to use to import the file.""" Importers = ( (_("Text separated by tabs or semicolons (*)"), TextImporter), (_("Packaged Anki Deck/Collection (*.apkg *.colpkg *.zip)"), AnkiPackageImporter), diff --git a/anki/importing/noteimp.py b/anki/importing/noteimp.py index b4ef9a85257..f51d1c4950f 100644 --- a/anki/importing/noteimp.py +++ b/anki/importing/noteimp.py @@ -47,7 +47,16 @@ def __init__(self): # 2: import even if first field matches existing note class NoteImporter(Importer): - + """TODO + + keyword arguments: + mapping -- A list of name of fields of model + model -- to which model(note type) the note will be imported. + _deckMap -- TODO + importMode -- 0 if data with similar first fields than a card in the db should be updated + 1 if they should be ignored + 2 if they should be added anyway + """ needMapper = True needDelimiter = False allowHTML = False @@ -66,10 +75,16 @@ def run(self): self.importNotes(c) def fields(self): - "The number of fields." + """The number of fields.""" + + #This should be overrided by concrete class, and never called directly return 0 def initMapping(self): + """Initial mapping. + + The nth element of the import is sent to nth field, if it exists + to tag otherwise""" flds = [f['name'] for f in self.model['flds']] # truncate to provided count flds = flds[0:self.fields()] @@ -81,6 +96,7 @@ def initMapping(self): self.mapping = flds def mappingOk(self): + """Whether something is mapped to the first field""" return self.model['flds'][0]['name'] in self.mapping def foreignNotes(self): @@ -107,7 +123,7 @@ def importNotes(self, notes): csums[csum].append(id) else: csums[csum] = [id] - firsts = {} + firsts = {}#mapping sending first field of added note to true fld0idx = self.mapping.index(self.model['flds'][0]['name']) self._fmap = self.col.models.fieldMap(self.model) self._nextID = timestampID(self.col.db, "notes") @@ -121,7 +137,7 @@ def importNotes(self, notes): self._cards = [] self._emptyNotes = False dupeCount = 0 - dupes = [] + dupes = []#List of first field seen, present in the db, and added anyway for n in notes: for c in range(len(n.fields)): if not self.allowHTML: @@ -146,7 +162,7 @@ def importNotes(self, notes): continue firsts[fld0] = True # already exists? - found = False + found = False#Whether a note with a similar first field was found if csum in csums: # csum is not a guarantee; have to check for id in csums[csum]: @@ -233,6 +249,7 @@ def newData(self, n): n.fieldsStr, "", "", 0, ""] def addNew(self, rows): + """Adds every notes of rows into the db""" self.col.db.executemany( "insert or replace into notes values (?,?,?,?,?,?,?,?,?,?,?)", rows) diff --git a/anki/latex.py b/anki/latex.py index 10d77d1e43f..a6a81eafc7e 100644 --- a/anki/latex.py +++ b/anki/latex.py @@ -29,6 +29,7 @@ os.environ['PATH'] += ":/usr/texbin:/Library/TeX/texbin" def stripLatex(text): + """The input text without its LaTeX environment.""" for match in regexps['standard'].finditer(text): text = text.replace(match.group(), "") for match in regexps['expression'].finditer(text): @@ -38,7 +39,21 @@ def stripLatex(text): return text def mungeQA(html, type, fields, model, data, col): - "Convert TEXT with embedded latex tags to image links." + """html, where LaTeX parts are replaced by some HTML. + + see _imgLink docstring regarding the rules for LaTeX media. + + keyword arguments: + html -- the text in which to find the LaTeX to be replaced. + type -- not used. "q" or "a" for question and answer + fields -- not used. A dictionnary containing Tags, Type(model + name), Deck, Subdeck(part after last ::), card: template + name... TODO (see collection._renderQA for more info) + model -- the model in which is compiled the note. It deals with + the header/footer, and the image file format + data -- not used. [cid, nid, mid, did, ord, tags, flds] + col -- the current collection. It deals with media folder + """ for match in regexps['standard'].finditer(html): html = html.replace(match.group(), _imgLink(col, match.group(1), model)) for match in regexps['expression'].finditer(html): @@ -51,7 +66,29 @@ def mungeQA(html, type, fields, model, data, col): return html def _imgLink(col, latex, model): - "Return an img link for LATEX, creating if necesssary." + """Some HTML to display instead of the LaTeX code. + + If some image already exists, related to this LaTeX code, an HTML + code showing this image is returned. + Otherwise, the latex code is compiled, and everything happen as in + the previous case. + + In case of compilation error, an error message explaining the + error (or asking whether program latex and dvipng/dvisvgm are + installed) is returned. + + During the compilation the compiled file is in tmp.tex and its + output (err and std) in latex_log.tex, replacing previous files of + the same name. Both of those file are in the tmpdir as in + utils.py. + + Keyword arguments: + col -- the current collection. It is used for the media folder (and + as argument for _latexFromHtml, where it seems to be useless) + latex -- the latex code to compile + model -- the model in which is compiled the note. It deals with + the header/footer, and the image file format. + """ txt = _latexFromHtml(col, latex) if model.get("latexsvg", False): @@ -76,12 +113,38 @@ def _imgLink(col, latex, model): return link def _latexFromHtml(col, latex): - "Convert entities and fix newlines." + """Convert entities and fix newlines. + + First argument is not used. + """ latex = re.sub("|

", "\n", latex) latex = stripHTML(latex) return latex def _buildImg(col, latex, fname, model): + """Generate an image file from latex code + + latex's header and foot is added as in the model. The image is + named fname and added to the media folder of this collection. + + The compiled file is in tmp.tex and its output (err and std) in + latex_log.tex, replacing previous files of the same name. Both of + those file are in the tmpdir as in utils.py + + Compiles to svg if latexsvg is set to true in the model, otherwise + to png. The compilation commands are given above. + + In case of error, return an error message to be displayed instead + of the LaTeX document. (Note that this image is not displayed in + AnkiDroid. It is probably shown only in computer mode) + + Keyword arguments: + col -- the current collection. It deals with media folder + latex -- the code LaTeX to compile, as given in fields + fname -- the name given to the generated png + model -- the model in which is compiled the note. It deals with + the header/footer, and the image file format + """ # add header/footer latex = (model["latexPre"] + "\n" + latex + "\n" + @@ -117,12 +180,12 @@ def _buildImg(col, latex, fname, model): oldcwd = os.getcwd() png = namedtmp("tmp.%s" % ext) try: - # generate png + # generate an image os.chdir(tmpdir()) for latexCmd in latexCmds: if call(latexCmd, stdout=log, stderr=log): return _errMsg(latexCmd[0], texpath) - # add to media + # add the image to the media folder shutil.copyfile(png, os.path.join(mdir, fname)) return finally: @@ -130,6 +193,15 @@ def _buildImg(col, latex, fname, model): log.close() def _errMsg(type, texpath): + """An error message, in html, concerning LaTeX compilation. + + This message contains LaTeX outputs if it exists, or a message + asking whether the program latex and dvipng/dvisvgm are installed. + + Keyword arguments + type -- the (begin of the) executed command + texpath -- the path to the (temporary) file which was compiled + """ msg = (_("Error executing %s.") % type) + "
" msg += (_("Generated file: %s") % texpath) + "
" try: @@ -144,3 +216,5 @@ def _errMsg(type, texpath): # setup q/a filter addHook("mungeQA", mungeQA) +#This hook is called collection._renderQA. See mungeQA comment to know +#the parameters diff --git a/anki/media.py b/anki/media.py index ff7f03e9665..5377af61974 100644 --- a/anki/media.py +++ b/anki/media.py @@ -1,6 +1,21 @@ # -*- coding: utf-8 -*- # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +""" +self.dir media's directory +self.col the collection +self.db the database for media (note that it's different from the collection database) + +Table media: +fname -- the name of the file in media directory +csum -- checksum of the content of the file last time it was checked. Null indicate deleted file +mtime -- time of last modification +dirty -- 0 if the data is not dirty, 1 if it is dirty + +table meta: a single entry, with: +dirMod -- date of the last modification of the media directory according to the os +lastUsn -- int synchronisation number of the last synchronisation +""" import io import re import traceback @@ -19,8 +34,15 @@ from anki.lang import _ class MediaManager: + """ + _dir -- the directory of media. Unless server is given to the constructor, in this cas it's None. Directory is changed to it during synchronization, and then changed back to previous directory. + _oldcwd -- the working directory when media manager is created. The directory is changed to this value when the MediaManager is closed. If server is given in the constructor, then it's None. +""" + + """Captures the argument foo of [sound:foo]""" soundRegexps = [r"(?i)(\[sound:(?P[^]]+)\])"] + """Captures the argument foo of , ignoring quotes around foo.""" imgRegexps = [ # src element quoted case r"(?i)(]* src=(?P[\"'])(?P[^>]+?)(?P=str)[^>]*>)", @@ -30,6 +52,10 @@ class MediaManager: regexps = soundRegexps + imgRegexps def __init__(self, col, server): + """ + TODO + + server -- always false in Anki""" self.col = col if server: self._dir = None @@ -51,6 +77,7 @@ def __init__(self, col, server): self.connect() def connect(self): + """Ensure the existence of a database in current format, connected in self.db.""" if self.col.server: return path = self.dir()+".db2" @@ -76,6 +103,7 @@ def _initDB(self): """) def maybeUpgrade(self): + """Upgrade database in old format to current format.""" oldpath = self.dir()+".db" if os.path.exists(oldpath): self.db.execute('attach "../collection.media.db" as old') @@ -103,6 +131,10 @@ def maybeUpgrade(self): os.rename("../collection.media.db", npath) def close(self): + """Close database connection. + + don't do anything if server is truthy. + change dir back to old working dir""" if self.col.server: return self.db.close() @@ -116,12 +148,14 @@ def close(self): pass def _deleteDB(self): + """Delete connected DB, connect to a new one""" path = self.db._path self.close() os.unlink(path) self.connect() def dir(self): + """The directory of media""" return self._dir def _isFAT32(self): @@ -142,10 +176,21 @@ def _isFAT32(self): # opath must be in unicode def addFile(self, opath): + """Copy the file at path opath to collection.media, + + Name may be changed to ensure unicity. + """ with open(opath, "rb") as f: return self.writeData(opath, f.read()) def writeData(self, opath, data, typeHint=None): + """Add data in the file of name opath in media dir. + + Only file name of opath is keep. + If file as no extension, and it is jpg or png according to typeHint, then add extension + Add a number extension if this name already exists + + """ # if fname is a full path, use only the basename fname = os.path.basename(opath) @@ -194,6 +239,21 @@ def repl(match): ########################################################################## def filesInStr(self, mid, string, includeRemote=False): + """The list of media's path in the string. + + Medias starting with _ are treated as any media. + + Each clozes are expanded in every possible ways. It allows + for different strings to be created. + + Concerning the part of the string related to LaTeX, media are + generated as explained in latex._imgLink's docstring + + Keyword arguments: + mid -- the id of the model of the note whose string is considered + string -- A string, which corresponds to a field of a note + includeRemote -- whether the list should include contents which is with http, https or ftp + """ l = [] model = self.col.models.get(mid) strings = [] @@ -216,34 +276,53 @@ def filesInStr(self, mid, string, includeRemote=False): return l def _expandClozes(self, string): + """The list of all strings, where the clozes are expanded. + + For each cloze number n, there is a string with cloze n replaced by [...] or by [hint], and every other clozes replaced by their text. + + There is also a text where each cloze are replaced by their value; i.e. the answer""" ords = set(re.findall(r"{{c(\d+)::.+?}}", string)) + #The set of clozes occurring in the string strings = [] from anki.template.template import clozeReg def qrepl(m): + """The text by which the cloze m must be replaced in the question.""" if m.group(4): return "[%s]" % m.group(4) else: return "[...]" + + if m.group(3): + return "[%s]" % m.group(3) + else: + return "[...]" def arepl(m): + """The text by which the cloze m must be replaced in the answer.""" return m.group(2) for ord in ords: s = re.sub(clozeReg%ord, qrepl, string) + #Replace the cloze number ord by the deletion s = re.sub(clozeReg%".+?", "\\2", s) + #Replace every other clozes by their content strings.append(s) strings.append(re.sub(clozeReg%".+?", arepl, string)) return strings def transformNames(self, txt, func): + """Apply func to all subtext matching the regexps txt.""" for reg in self.regexps: txt = re.sub(reg, func, txt) return txt def strip(self, txt): + """Delete all text matching the regexps txt""" for reg in self.regexps: txt = re.sub(reg, "", txt) return txt def escapeImages(self, string, unescape=False): + """Replace local image url by replacing special character by the + escape %xx or reciprocally depending on unescape value.""" if unescape: fn = urllib.parse.unquote else: @@ -262,7 +341,7 @@ def repl(match): ########################################################################## def check(self, local=None): - "Return (missingFiles, unusedFiles)." + "Return (missingFiles, unusedFiles, warnings)." mdir = self.dir() # gather all media references in NFC form allRefs = set() @@ -347,6 +426,7 @@ def _normalizeNoteRefs(self, nid): ########################################################################## def have(self, fname): + """Whether a fil with name fname exists in the media directory""" return os.path.exists(os.path.join(self.dir(), fname)) # Illegal characters and paths @@ -355,9 +435,13 @@ def have(self, fname): _illegalCharReg = re.compile(r'[][><:"/?*^\\|\0\r\n]') def stripIllegal(self, str): + """str, without its illegal characters""" return re.sub(self._illegalCharReg, "", str) def hasIllegal(self, str): + """Whether str contains a illegal character. + + Either according to _illegalCharReg, or because it can't be encoded if file system encoding""" if re.search(self._illegalCharReg, str): return True try: @@ -417,17 +501,22 @@ def _cleanLongFilename(self, fname): ########################################################################## def findChanges(self): - "Scan the media folder if it's changed, and note any changes." + "Scan the media folder if it's changed, and note any changes in the db." if self._changed(): self._logChanges() def haveDirty(self): + """Whether the database has at least one dirty element""" return self.db.scalar("select 1 from media where dirty=1 limit 1") def _mtime(self, path): + """Time of most recent content modification of file at path. + + Expressed in seconds.""" return int(os.stat(path).st_mtime) def _checksum(self, path): + """Checksum of file at path""" with open(path, "rb") as f: return checksum(f.read()) @@ -523,6 +612,7 @@ def setLastUsn(self, usn): self.db.commit() def syncInfo(self, fname): + """(Checkusm, dirty number) from media with name fname""" ret = self.db.first( "select csum, dirty from media where fname=?", fname) return ret or (None, 0) @@ -533,15 +623,20 @@ def markClean(self, fnames): "update media set dirty=0 where fname=?", fname) def syncDelete(self, fname): + """Delete the file fname if it is not in media directory.""" if os.path.exists(fname): os.unlink(fname) self.db.execute("delete from media where fname=?", fname) def mediaCount(self): + """Number of media according to database""" return self.db.scalar( "select count() from media where csum is not null") def dirtyCount(self): + """Number of dirty media according to database. + + (couting the one potentially deleted)""" return self.db.scalar( "select count() from media where dirty=1") @@ -558,6 +653,14 @@ def forceResync(self): ########################################################################## def mediaChangesZip(self): + """ + The pair with: + * A string encoding a zip files with: + ** media to upload + ** _meta: a json list associating to each name (as in zip) to + the real name of the file + * list of media considered + """ f = io.BytesIO() z = zipfile.ZipFile(f, "w", compression=zipfile.ZIP_DEFLATED) @@ -565,8 +668,9 @@ def mediaChangesZip(self): # meta is list of (fname, zipname), where zipname of None # is a deleted file meta = [] - sz = 0 + sz = 0#sum of the size of the media. + # loop over dirty medias. At most SYNC_ZIP_COUNT = 25 elements for c, (fname, csum) in enumerate(self.db.execute( "select fname, csum from media where dirty=1" " limit %d"%SYNC_ZIP_COUNT)): @@ -591,7 +695,15 @@ def mediaChangesZip(self): return f.getvalue(), fnames def addFilesFromZip(self, zipData): - "Extract zip data; true if finished." + """ + Copy each file from zipData (except _meta) to the media + folder, and add those files to the media database. Rename the + file according to _meta. + + zipData -- A byte tream containing a zipfile, containing: + * _meta, a file containing a json dict associtaing to each name of file in zip (except meta) a name to be used in the media folder + * arbitrary fields to save in the media folder + """ f = io.BytesIO(zipData) z = zipfile.ZipFile(f, "r") media = [] diff --git a/anki/models.py b/anki/models.py index d37b7e24ea0..a20cc66ecbe 100644 --- a/anki/models.py +++ b/anki/models.py @@ -2,6 +2,64 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +"""This module deals with models, known as note type in Anki's documentation. + +A model is composed of: +css -- CSS, shared for all templates of the model +did -- Long specifying the id of the deck that cards are added to by +default +flds -- JSONArray containing object for each field in the model. See flds +id -- model ID, matches notes.mid +latexPost -- String added to end of LaTeX expressions (usually \\end{document}), +latexPre -- preamble for LaTeX expressions, +mod -- modification time in milliseconds, +name -- the name of the model, +req -- Array of arrays describing which fields are required. See req +sortf -- Integer specifying which field is used for sorting in the +browser, +tags -- Anki saves the tags of the last added note to the current +model, use an empty array [], +tmpls -- The list of templates. See below + -- In db:JSONArray containing object of CardTemplate for each card in +model. +type -- Integer specifying what type of model. 0 for standard, 1 for +cloze, +usn -- Update sequence number: used in same way as other usn vales in +db, +vers -- Legacy version number (unused), use an empty array [] +changed -- Whether the Model has been changed and should be written in +the database.""" + + +"""A field object (flds) is an array composed of: +font -- "display font", +media -- "array of media. appears to be unused", +name -- "field name", +ord -- "ordinal of the field - goes from 0 to num fields -1", +rtl -- "boolean, right-to-left script", +size -- "font size", +sticky -- "sticky fields retain the value that was last added +when adding new notes" """ + +"""req' fields are: +"the 'ord' value of the template object from the 'tmpls' array you are setting the required fields of", +'? string, "all" or "any"', +["? another array of 'ord' values from field object you +want to require from the 'flds' array"]""" + + +"""tmpls (a template): a dict with +afmt -- "answer template string", +bafmt -- "browser answer format: +used for displaying answer in browser", +bqfmt -- "browser question format: +used for displaying question in browser", +did -- "deck override (null by default)", +name -- "template name", +ord -- "template number, see flds", +qfmt -- "question format string" +""" + import copy, re, json from anki.utils import intTime, joinFields, splitFields, ids2str,\ checksum @@ -70,11 +128,13 @@ } class ModelManager: - + """This object is usually denoted mm as a variable. Or .models in + collection.""" # Saving/loading registry ############################################################# def __init__(self, col): + """Returns a ModelManager whose collection is col.""" self.col = col def load(self, json_): @@ -83,7 +143,15 @@ def load(self, json_): self.models = json.loads(json_) def save(self, m=None, templates=False): - "Mark M modified if provided, and schedule registry flush." + """ + * Mark m modified if provided. + * Schedule registry flush. + * Calls hook newModel + + Keyword arguments: + m -- A Model + templates -- whether to check for cards not generated in this model + """ if m and m['id']: m['mod'] = intTime() m['usn'] = self.col.usn() @@ -91,7 +159,7 @@ def save(self, m=None, templates=False): if templates: self._syncTemplates(m) self.changed = True - runHook("newModel") + runHook("newModel") # By default, only refresh side bar of browser def flush(self): "Flush the registry if any models were changed." @@ -111,31 +179,46 @@ def ensureNotEmpty(self): ############################################################# def current(self, forDeck=True): - "Get current model." + """Get current model. + + This mode is first considered using the current deck's mid, if + forDeck is true(default). + + Otherwise, the curModel configuration value is used. + + Otherwise, the first model is used. + + Keyword arguments: + forDeck -- Whether ther model of the deck should be considered; assuming it exists.""" m = self.get(self.col.decks.current().get('mid')) if not forDeck or not m: m = self.get(self.col.conf['curModel']) return m or list(self.models.values())[0] def setCurrent(self, m): + """Change curModel value and marks the collection as modified.""" self.col.conf['curModel'] = m['id'] self.col.setMod() def get(self, id): - "Get model with ID, or None." + "Get model object with ID, or None." id = str(id) if id in self.models: return self.models[id] def all(self): - "Get all models." + "Get all model objects." return list(self.models.values()) def allNames(self): + "Get all model names." return [m['name'] for m in self.all()] def byName(self, name): - "Get model with NAME." + """Get model whose name is name. + + keyword arguments + name -- the name of the wanted model.""" for m in list(self.models.values()): if m['name'] == name: return m @@ -168,12 +251,20 @@ def rem(self, m): self.setCurrent(list(self.models.values())[0]) def add(self, m): + """Add a new model m in the database of models""" self._setID(m) self.update(m) self.setCurrent(m) self.save(m) def ensureNameUnique(self, m): + """Transform the name of m into a new name. + + If a model with this name but a distinct id exists in the + manager, the name of this object is appended by - and by a + 5 random digits generated using the current time. + Keyword arguments + m -- a model object""" for mcur in self.all(): if (mcur['name'] == m['name'] and mcur['id'] != m['id']): m['name'] += "-" + checksum(str(time.time()))[:5] @@ -187,6 +278,7 @@ def update(self, m): self.save() def _setID(self, m): + """Set the id of m to a new unique value.""" while 1: id = str(intTime(1000)) if id not in self.models: @@ -194,25 +286,38 @@ def _setID(self, m): m['id'] = id def have(self, id): + """Whether there exists a model whose id is did.""" return str(id) in self.models def ids(self): + """The list of id of models""" return list(self.models.keys()) # Tools ################################################## def nids(self, m): - "Note ids for M." + """The ids of notes whose model is m. + + Keyword arguments + m -- a model object.""" return self.col.db.list( "select id from notes where mid = ?", m['id']) def useCount(self, m): - "Number of note using M." + """Number of note using the model m. + + Keyword arguments + m -- a model object.""" return self.col.db.scalar( "select count() from notes where mid = ?", m['id']) def tmplUseCount(self, m, ord): + """The number of cards which used template number ord of the + model obj. + + Keyword arguments + m -- a model object.""" return self.col.db.scalar(""" select count() from cards, notes where cards.nid = notes.id and notes.mid = ? and cards.ord = ?""", m['id'], ord) @@ -221,7 +326,7 @@ def tmplUseCount(self, m, ord): ################################################## def copy(self, m): - "Copy, save and return." + "A copy of m, already in the collection." m2 = copy.deepcopy(m) m2['name'] = _("%s copy") % m2['name'] self.add(m2) @@ -231,21 +336,35 @@ def copy(self, m): ################################################## def newField(self, name): + """A new field, similar to the default one, whose name is name.""" f = defaultField.copy() f['name'] = name return f def fieldMap(self, m): - "Mapping of field name -> (ord, field)." + """Mapping of (field name) -> (ord, field object). + + keyword arguments: + m : a model + """ return dict((f['name'], (f['ord'], f)) for f in m['flds']) def fieldNames(self, m): + """The list of names of fields of this model.""" return [f['name'] for f in m['flds']] def sortIdx(self, m): + """The index of the field used for sorting.""" return m['sortf'] def setSortIdx(self, m, idx): + """State that the id of the sorting field of the model is idx. + + Mark the model as modified, change the cache. + Keyword arguments + m -- a model + idx -- the identifier of a field + """ assert 0 <= idx < len(m['flds']) self.col.modSchema(check=True) m['sortf'] = idx @@ -253,6 +372,14 @@ def setSortIdx(self, m, idx): self.save(m) def addField(self, m, field): + """Append the field field as last element of the model m. + + todo + + Keyword arguments + m -- a model + field -- a field object + """ # only mod schema if model isn't new if m['id']: self.col.modSchema(check=True) @@ -265,6 +392,14 @@ def add(fields): self._transformFields(m, add) def remField(self, m, field): + """Remove a field from a model. + Also remove it from each note of this model + Move the position of the sortfield. Update the position of each field. + + Modify the template + + m -- the model + field -- the field object""" self.col.modSchema(check=True) # save old sort field sortFldName = m['flds'][m['sortf']]['name'] @@ -288,6 +423,11 @@ def delete(fields): self.renameField(m, field, None) def moveField(self, m, field, idx): + """Move the field to position idx + + idx -- new position, integer + field -- a field object + """ self.col.modSchema(check=True) oldidx = m['flds'].index(field) if oldidx == idx: @@ -309,7 +449,16 @@ def move(fields, oldidx=oldidx): self._transformFields(m, move) def renameField(self, m, field, newName): + """Rename the field. In each template, find the mustache related to + this field and change them. + + m -- the model dictionnary + field -- the field dictionnary + newName -- either a name. Or None if the field is deleted. + + """ self.col.modSchema(check=True) + #Regexp associating to a mustache the name of its field pat = r'{{([^{}]*)([:#^/]|[^:#/^}][^:}]*?:|)%s}}' def wrap(txt): def repl(match): @@ -327,10 +476,20 @@ def repl(match): self.save(m) def _updateFieldOrds(self, m): + """ + Change the order of the field of the model in order to copy + the order in m['flds']. + + Keyword arguments + m -- a model""" for c, f in enumerate(m['flds']): f['ord'] = c def _transformFields(self, m, fn): + """For each note of the model m, apply m to the set of field's value, + and save the note modified. + + fn -- a function taking and returning a list of field.""" # model hasn't been added yet? if not m['id']: return @@ -346,12 +505,22 @@ def _transformFields(self, m, fn): ################################################## def newTemplate(self, name): + """A new template, whose content is the one of + defaultTemplate, and name is name. + + It's used in order to import mnemosyn, and create the standard + model during anki's first initialization. It's not used in day to day anki. + """ t = defaultTemplate.copy() t['name'] = name return t def addTemplate(self, m, template): - "Note: should col.genCards() afterwards." + """Add a new template in m, as last element. This template is a copy + of the input template + """ + + "Note: should call col.genCards() afterwards." if m['id']: self.col.modSchema(check=True) m['tmpls'].append(template) @@ -359,7 +528,11 @@ def addTemplate(self, m, template): self.save(m) def remTemplate(self, m, template): - "False if removing template would leave orphan notes." + """Remove the input template from the model m. + + Return False if removing template would leave orphan + notes. Otherwise True + """ assert len(m['tmpls']) > 1 # find cards using this template ord = m['tmpls'].index(template) @@ -389,10 +562,17 @@ def remTemplate(self, m, template): return True def _updateTemplOrds(self, m): + """Change the value of 'ord' in each template of this model to reflect its new position""" for c, t in enumerate(m['tmpls']): t['ord'] = c def moveTemplate(self, m, template, idx): + """Move input template to position idx in m. + + Move also every other template to make this consistent. + + Comment again after that TODODODO + """ oldidx = m['tmpls'].index(template) if oldidx == idx: return @@ -412,6 +592,9 @@ def moveTemplate(self, m, template, idx): self.col.usn(), intTime(), m['id']) def _syncTemplates(self, m): + """Generate all cards not yet generated, whose note's model is m. + + It's called only when model is saved, a new model is given and template is asked to be computed""" rem = self.col.genCards(self.nids(m)) # Model changing @@ -420,6 +603,17 @@ def _syncTemplates(self, m): # - newModel should be self if model is not changing def change(self, m, nids, newModel, fmap, cmap): + """Change the model of the nodes in nids to newModel + + currently, fmap and cmap are null only for tests. + + keyword arguments + m -- the previous model of the notes + nids -- a list of id of notes whose model is m + newModel -- the model to which the cards must be converted + fmap -- the dictionnary sending to each fields'ord of the old model a field'ord of the new model + cmap -- the dictionnary sending to each card type's ord of the old model a card type's ord of the new model + """ self.col.modSchema(check=True) assert newModel['id'] == m['id'] or (fmap and cmap) if fmap: @@ -429,7 +623,17 @@ def change(self, m, nids, newModel, fmap, cmap): self.col.genCards(nids) def _changeNotes(self, nids, newModel, map): - d = [] + """Change the note whose ids are nid to the model newModel, reorder + fields according to map. Write the change in the database + + Note that if a field is mapped to nothing, it is lost + + keyword arguments: + nids -- the list of id of notes to change + newmodel -- the model of destination of the note + map -- the dictionnary sending to each fields'ord of the old model a field'ord of the new model + """ + d = [] #The list of dictionnaries, containing the information relating to the new cards nfields = len(newModel['flds']) for (nid, flds) in self.col.db.execute( "select id, flds from notes where id in "+ids2str(nids)): @@ -448,6 +652,20 @@ def _changeNotes(self, nids, newModel, map): self.col.updateFieldCache(nids) def _changeCards(self, nids, oldModel, newModel, map): + """Change the note whose ids are nid to the model newModel, reorder + fields according to map. Write the change in the database + + Remove the cards mapped to nothing + + If the source is a cloze, it is (currently?) mapped to the + card of same order in newModel, independtly of map. + + keyword arguments: + nids -- the list of id of notes to change + oldModel -- the soruce model of the notes + newmodel -- the model of destination of the notes + map -- the dictionnary sending to each card 'ord of the old model a card'ord of the new model or to None + """ d = [] deleted = [] for (cid, ord) in self.col.db.execute( @@ -490,6 +708,7 @@ def scmhash(self, m): ########################################################################## def _updateRequired(self, m): + """Entirely recompute the model's req value""" if m['type'] == MODEL_CLOZE: # nothing to do return @@ -501,15 +720,24 @@ def _updateRequired(self, m): m['req'] = req def _reqForTemplate(self, m, flds, t): + """A rule which is supposed to determine whether a card should be + generated or not according to its fields. + + See ../documentation/templates_generation_rules.md + + """ a = [] b = [] for f in flds: a.append("ankiflag") b.append("") data = [1, 1, m['id'], 1, t['ord'], "", joinFields(a), 0] + # The html of the card at position ord where each field's content is "ankiflag" full = self.col._renderQA(data)['q'] data = [1, 1, m['id'], 1, t['ord'], "", joinFields(b), 0] + # The html of the card at position ord where each field's content is the empty string "" empty = self.col._renderQA(data)['q'] + # if full and empty are the same, the template is invalid and there is # no way to satisfy it if full == empty: @@ -538,13 +766,17 @@ def _reqForTemplate(self, m, flds, t): return type, req def availOrds(self, m, flds): - "Given a joined field string, return available template ordinals." + """Given a joined field string, return ordinal of card type which + should be generated. See + ../documentation/templates_generation_rules.md for the detail + + """ if m['type'] == MODEL_CLOZE: return self._availClozeOrds(m, flds) fields = {} for c, f in enumerate(splitFields(flds)): fields[c] = f.strip() - avail = [] + avail = []#List of ord of cards which would be generated for ord, type, req in m['req']: # unsatisfiable template if type == "none": @@ -572,18 +804,27 @@ def availOrds(self, m, flds): return avail def _availClozeOrds(self, m, flds, allowEmpty=True): + """The list of fields F which are used in some {{cloze:F}} in a template + + keyword arguments: + m: a model + flds: a list of fields as in the database + allowEmpty: allows to treat a note without cloze field as a note with a cloze number 1 + """ sflds = splitFields(flds) - map = self.fieldMap(m) - ords = set() + map = self.fieldMap(m)#dictionnary (field name) -> (ord, field object) + ords = set()#Will eventually contain the set of number of + #clozes in cloze fields (whether a field is a + #cloze field is determined according to template) matches = re.findall("{{[^}]*?cloze:(?:[^}]?:)*(.+?)}}", m['tmpls'][0]['qfmt']) matches += re.findall("<%cloze:(.+?)%>", m['tmpls'][0]['qfmt']) for fname in matches: if fname not in map: - continue + continue#Do not consider cloze not related to an existing field ord = map[fname][0] ords.update([int(m)-1 for m in re.findall( - r"(?s){{c(\d+)::.+?}}", sflds[ord])]) - if -1 in ords: + r"(?s){{c(\d+)::.+?}}", sflds[ord])])#The number of the cloze of this field, minus one + if -1 in ords:#remove cloze 0 ords.remove(-1) if not ords and allowEmpty: # empty clozes use first ord diff --git a/anki/notes.py b/anki/notes.py index 63a46dc8ef5..b6ae365ebab 100644 --- a/anki/notes.py +++ b/anki/notes.py @@ -7,7 +7,41 @@ class Note: + """A note is composed of: + + id -- epoch seconds of when the note was created. A unique id + guid -- globally unique id, almost certainly used for syncing + mid -- model id + mod -- modification timestamp, epoch seconds + usn -- update sequence number: see readme.synchronization for more info + tags -- List of tags. + -- In the database, it is a space-separated string of tags. + -- includes space at the beginning and end, for LIKE "% tag %" queries + fields -- the list of values of the fields in this note. + in the db, instead of fields, there is flds; which is the content of fields, in the order of the note type, concatenated using \x1f (\\x1f)) + sfld -- sort field: used for quick sorting and duplicate check + csum -- field checksum used for duplicate check. + -- integer representation of first 8 digits of sha1 hash of the first field + flags-- unused + data -- unused + + Not in the database: + col -- its collection + _model -- the model object + _fmap -- Mapping of (field name) -> (ord, field object). See models.py for field objects + scm -- schema mod time: time when "schema" was modified. As in the collection. + newlyAdded -- used by flush, to see whether a note is new or not. + """ def __init__(self, col, model=None, id=None): + """A note. + + Exactly one of model and id should be set. Not both. + + keyword arguments: + id -- a note id. In this case, current note is the note with this id + model -- A model object. In which case the note the note use this model. + + """ assert not (model and id) self.col = col if id: @@ -26,6 +60,8 @@ def __init__(self, col, model=None, id=None): self.scm = self.col.scm def load(self): + """Given a note knowing its collection and its id, choosing this + card from the database.""" (self.guid, self.mid, self.mod, @@ -43,7 +79,17 @@ def load(self): self.scm = self.col.scm def flush(self, mod=None): - "If fields or tags have changed, write changes to disk." + """If fields or tags have changed, write changes to disk. + + If there exists a note with same id, tags and fields, and mod is not set, do nothing. + Change the mod to given argument or current time + Change the USNk + If the note is not new, according to _preFlush, generate the cards + Add its tag to the collection + Add the note in the db + + Keyword arguments: + mod -- A modification timestamp""" assert self.scm == self.col.scm self._preFlush() sfld = stripHTMLMedia(self.fields[self.col.models.sortIdx(self._model)]) @@ -66,56 +112,72 @@ def flush(self, mod=None): self._postFlush() def joinedFields(self): + """The list of fields, separated by \x1f (\\x1f).""" return joinFields(self.fields) def cards(self): + """The list of cards objects associated to this note.""" return [self.col.getCard(id) for id in self.col.db.list( "select id from cards where nid = ? order by ord", self.id)] def model(self): + """The model object of this card.""" return self._model # Dict interface ################################################## def keys(self): + """The list of field names of this note.""" return list(self._fmap.keys()) def values(self): + """The list of value of this note's fields.""" return self.fields def items(self): + """The list of (name, value), for each field of the note.""" return [(f['name'], self.fields[ord]) for ord, f in sorted(self._fmap.values())] def _fieldOrd(self, key): + """The order of the key in the note.""" try: return self._fmap[key][0] except: raise KeyError(key) def __getitem__(self, key): + """The value of the field key.""" return self.fields[self._fieldOrd(key)] def __setitem__(self, key, value): + """Set the value of the field key to value.""" self.fields[self._fieldOrd(key)] = value def __contains__(self, key): + """Whether key is a field of this note.""" return key in list(self._fmap.keys()) # Tags ################################################## def hasTag(self, tag): + """Whether tag is a tag of this note.""" return self.col.tags.inList(tag, self.tags) def stringTags(self): + """A string containing the tags, canonified, separated with white +space, with an initial and a final white space.""" return self.col.tags.join(self.col.tags.canonify(self.tags)) def setTagsFromStr(self, str): + """Set the list of tags of this note using the str.""" self.tags = self.col.tags.split(str) def delTag(self, tag): + """Remove every occurence of tag in this note's tag. Case + insensitive.""" rem = [] for t in self.tags: if t.lower() == tag.lower(): @@ -124,7 +186,10 @@ def delTag(self, tag): self.tags.remove(r) def addTag(self, tag): - # duplicates will be stripped on save + """Add tag to the list of note's tag. + + duplicates will be stripped on save. + """ self.tags.append(tag) # Unique/duplicate check @@ -149,12 +214,16 @@ def dupeOrEmpty(self): ################################################## def _preFlush(self): + """Set newlyAdded to: whether at least one card of this note belongs in +the db.""" # have we been added yet? self.newlyAdded = not self.col.db.scalar( "select 1 from cards where nid = ?", self.id) def _postFlush(self): - # generate missing cards + """Generate cards for non-empty template of this note. + + Not executed if this note is newlyAdded.""" if not self.newlyAdded: rem = self.col.genCards([self.id]) # popping up a dialog while editing is confusing; instead we can diff --git a/anki/sched.py b/anki/sched.py index 785d75ea99a..f4f78e3f94d 100644 --- a/anki/sched.py +++ b/anki/sched.py @@ -19,6 +19,20 @@ # positive revlog intervals are in days (rev), negative in seconds (lrn) class Scheduler: + """ + today -- difference between the last time scheduler is seen and creation of the collection. + dayCutoff -- The timestamp of when today end. + reportLimit -- the maximal number to show in main windows + lrnCount -- The number of cards in learning in selected decks + revCount -- number of cards to review today in selected decks + newCount -- number of new cards to see today in selected decks + _lrnDids, _revDids, _newDids -- a copy of the set of active decks where decks with no card to see today are removed. + _newQueue, _lrnQueue, _revQueue -- list of ids of cards in the queue new, lrn and rev. At most queue limit (i.e. 50) + queueLimit -- maximum number of cards to queue simultaneously. Always 50 unless changed by an addon. + _lrnDayQueue -- todo + newCardModulus -- each card in position 0 Modulo newCardModulus is a new card. Or it is 0 if new cards are not mixed with reviews. + -- _haveQueues -- whether the number of cards to see today for current decks have been set. + """ name = "std" haveCustomStudy = True _spreadRev = True @@ -48,6 +62,11 @@ def getCard(self): return card def reset(self): + """ + Deal with the fact that it's potentially a new day. + Reset number of learning, review, new cards according to current decks + empty queues. Set haveQueues to true + """ self._updateCutoff() self._resetLrn() self._resetRev() @@ -55,6 +74,11 @@ def reset(self): self._haveQueues = True def answerCard(self, card, ease): + """Change the number of card to see in the decks and its + ancestors. Change the due/interval/ease factor of this card, + according to the button ease. + + """ self.col.log() assert 1 <= ease <= 4 self.col.markReview(card) @@ -81,19 +105,25 @@ def answerCard(self, card, ease): self._updateStats(card, 'new') if card.queue in (QUEUE_LRN, QUEUE_DAY_LRN): self._answerLrnCard(card, ease) - if not wasNewQ: + if not wasNewQ:# if wasNewQ holds, updating already + # happened above self._updateStats(card, 'lrn') elif card.queue == QUEUE_REV: self._answerRevCard(card, ease) self._updateStats(card, 'rev') else: - raise Exception("Invalid queue") + raise Exception(f"Invalid queue") self._updateStats(card, 'time', card.timeTaken()) card.mod = intTime() card.usn = self.col.usn() card.flushSched() def counts(self, card=None): + """The three numbers to show in anki deck's list/footer. + Number of new cards, learning repetition, review card. + + If cards, then the tuple takes into account the card. + """ counts = [self.newCount, self.lrnCount, self.revCount] if card: idx = self.countIdx(card) @@ -122,6 +152,8 @@ def dueForecast(self, days=7): return ret def countIdx(self, card): + """In which column the card in the queue should be counted. + day_lrn is sent to lrn, otherwise its the identity""" if card.queue == QUEUE_DAY_LRN: return QUEUE_LRN return card.queue @@ -161,6 +193,13 @@ def unburyCardsForDeck(self): ########################################################################## def _updateStats(self, card, type, cnt=1): + """Change the number of review/new/learn cards to see today in card's + deck, and in all of its ancestors. The number of card to see + is decreased by cnt. + + if type is time, it adds instead the time taken in this card + to this decks and all of its ancestors. + """ key = type+"Today" for g in ([self.col.decks.get(card.did)] + self.col.decks.parents(card.did)): @@ -169,6 +208,11 @@ def _updateStats(self, card, type, cnt=1): self.col.decks.save(g) def extendLimits(self, new, rev): + """Decrease the limit of new/rev card to see today to this deck, its + ancestors and all of its descendant, by new/rev. + + This number is called from aqt.customstudy.CustomStudy.accept, with the number of card to study today. + """ cur = self.col.decks.current() parents = self.col.decks.parents(cur['id']) children = [self.col.decks.get(did) for (name, did) in @@ -180,8 +224,12 @@ def extendLimits(self, new, rev): self.col.decks.save(g) def _walkingCount(self, limFn=None, cntFn=None): + """The sum of cntFn applied to each active deck. + + limFn -- function which associate to each deck obejct the maximum number of card to consider + cntFn -- function which, given a deck id and a limit, return a number of card at most equal to this limit.""" tot = 0 - pcounts = {} + pcounts = {}# Associate from each id of a parent deck p, the maximal number of cards of deck p which can be seen, minus the card found for its descendant already considered # for each of the active decks nameMap = self.col.decks.nameMap() for did in self.col.decks.active(): @@ -214,11 +262,16 @@ def _walkingCount(self, limFn=None, cntFn=None): ########################################################################## def deckDueList(self): - "Returns [deckname, did, rev, lrn, new]" + """ + Similar to nodes, without the recursive counting, with the full deck name + + [deckname (with ::), + did, rev, lrn, new (not counting subdeck)]""" self._checkDay() self.col.decks.checkIntegrity() decks = self.col.decks.all() decks.sort(key=itemgetter('name')) + #lims -- associating to each deck maximum number of new card and of review. Taking custom study into account lims = {} data = [] def parent(name): @@ -230,6 +283,7 @@ def parent(name): for deck in decks: p = parent(deck['name']) # new + #nlim -- maximal number of new card, taking parent into account nlim = self._deckNewLimitSingle(deck) if p: nlim = min(nlim, lims[p][0]) @@ -237,6 +291,7 @@ def parent(name): # learning lrn = self._lrnForDeck(deck['id']) # reviews + #rlim -- maximal number of review, taking parent into account rlim = self._deckRevLimitSingle(deck) if p: rlim = min(rlim, lims[p][1]) @@ -248,9 +303,22 @@ def parent(name): return data def deckDueTree(self): - return self._groupChildren(self.deckDueList()) + """Generate the node of the main deck. See deckbroser introduction to see what a node is + """ + #something similar to nodes, but without the recursive part + nodes_=self.deckDueList() + #the actual nodes + nodes=self._groupChildren(nodes_) + return nodes def _groupChildren(self, grps): + """[subdeck name without parent parts, + did, rev, lrn, new (counting subdecks) + [recursively the same things for the children]] + + Keyword arguments: + grps -- [deckname, did, rev, lrn, new] + """ # first, split the group names into components for g in grps: g[0] = g[0].split("::") @@ -260,6 +328,13 @@ def _groupChildren(self, grps): return self._groupChildrenMain(grps) def _groupChildrenMain(self, grps): + """ + [subdeck name without parent parts, + did, rev, lrn, new (counting subdecks) + [recursively the same things for the children]] + + keyword arguments: + grps -- [[subdeck], did, rev, lrn, new] sorted according to the list subdeck. Number for the subdeck precisely""" tree = [] # group and recurse def key(grp): @@ -330,18 +405,27 @@ def _getCard(self): ########################################################################## def _resetNewCount(self): - cntFn = lambda did, lim: self.col.db.scalar(f""" + """Set newCount to the counter of new cards for the active decks.""" + # Number of card in deck did, at most lim + def cntFn(did, lim): + ret = self.col.db.scalar(f""" select count() from (select 1 from cards where did = ? and queue = {QUEUE_NEW_CRAM} limit ?)""", did, lim) + return ret self.newCount = self._walkingCount(self._deckNewLimitSingle, cntFn) def _resetNew(self): + """Set newCount, newDids, newCardModulus. Empty newQueue. """ self._resetNewCount() self._newDids = self.col.decks.active()[:] self._newQueue = [] self._updateNewCardRatio() def _fillNew(self): + """Whether there are new card in current decks to see today. + + If it is the case that the _newQueue is not empty + """ if self._newQueue: return True if not self.newCount: @@ -371,6 +455,8 @@ def _getNewCard(self): return self.col.getCard(self._newQueue.pop()) def _updateNewCardRatio(self): + """set newCardModulus so that new cards are regularly mixed with review cards. At least 2. + Only if new and review should be mixed""" if self.col.conf['newSpread'] == NEW_CARDS_DISTRIBUTE: if self.newCount: self.newCardModulus = ( @@ -407,7 +493,11 @@ def _deckNewLimit(self, did, fn=None): return lim def _newForDeck(self, did, lim): - "New count for a single deck." + """The minimum between the number of new cards in this deck lim and self.reportLimit. + + keyword arguments: + did -- id of a deck + lim -- an upper bound for the returned number (in practice, the number of new cards to see by day for the deck's option) """ if not lim: return 0 lim = min(lim, self.reportLimit) @@ -416,11 +506,19 @@ def _newForDeck(self, did, lim): (select 1 from cards where did = ? and queue = {QUEUE_NEW_CRAM} limit ?)""", did, lim) def _deckNewLimitSingle(self, g): - "Limit for deck without parent limits." + """Maximum number of new card to see today for deck g, not considering parent limit. + + If g is a dynamic deck, then reportLimit. + Otherwise the number of card to see in this deck option, plus the number of card exceptionnaly added to this deck today. + + keyword arguments: + g -- a deck dictionnary + """ if g['dyn']: return self.reportLimit c = self.col.decks.confForDid(g['id']) - return max(0, c['new']['perDay'] - g['newToday'][1]) + ret = max(0, c['new']['perDay'] - g['newToday'][1]) + return ret def totalNewForCurrentDeck(self): return self.col.db.scalar( @@ -433,19 +531,21 @@ def totalNewForCurrentDeck(self): ########################################################################## def _resetLrnCount(self): - # sub-day + """Set lrnCount""" + # Number of reps which are due today, last seen today caped by report limit, in the selected decks self.lrnCount = self.col.db.scalar(f""" select sum(left/1000) from (select left from cards where did in %s and queue = {QUEUE_LRN} and due < ? limit %d)""" % ( self._deckLimit(), self.reportLimit), self.dayCutoff) or 0 - # day + # Number of cards in learning which are due today, last seen another day caped by report limit, in the selected decks self.lrnCount += self.col.db.scalar(f""" select count() from cards where did in %s and queue = {QUEUE_DAY_LRN} and due <= ? limit %d""" % (self._deckLimit(), self.reportLimit), self.today) def _resetLrn(self): + """Set lrnCount and _lrnDids. Empty _lrnQueue, lrnDayQueu.""" self._resetLrnCount() self._lrnQueue = [] self._lrnDayQueue = [] @@ -569,6 +669,11 @@ def _answerLrnCard(self, card, ease): self._logLrn(card, ease, conf, leaving, type, lastLeft) def _delayForGrade(self, conf, left): + """The number of second for the delay until the next time the card can + be reviewed. Assuming the number of left steps is left, + according to configuration conf + + """ left = left % 1000 try: delay = conf['delays'][-left] @@ -581,12 +686,27 @@ def _delayForGrade(self, conf, left): return delay*60 def _lrnConf(self, card): + """ lapse configuration if the card was due(i.e. review card + ?), otherwise new configuration. I don't get the point""" if card.type == CARD_DUE: return self._lapseConf(card) else: return self._newConf(card) def _rescheduleAsRev(self, card, conf, early): + """ Change schedule for graduation. + + If it's filtered without rescheduling, remove the card from + filtered and do nothing else. + + If it's lapsed, set the due date to tomorrow. Do not change + the interval. + + If it's a new card, change interval according to + _rescheduleNew. I.e. conf['ints'][1 if early + else 0]", change the due date accordingly. Change the easyness + to initial one. + """ lapse = card.type == CARD_DUE if lapse: if self._resched(card): @@ -596,6 +716,8 @@ def _rescheduleAsRev(self, card, conf, early): card.odue = 0 else: self._rescheduleNew(card, conf, early) + # Interval is now set. We must deal with queue and moving deck + # if dynamic. card.queue = QUEUE_REV card.type = CARD_DUE # if we were dynamic, graduating means moving back to the old deck @@ -611,6 +733,9 @@ def _rescheduleAsRev(self, card, conf, early): card.due = self.col.nextID("pos") def _startingLeft(self, card): + """(number of review to see, number which can be seen today) + But instead of a pair (a,b), returns a+1000*b. + """ if card.type == CARD_DUE: conf = self._lapseConf(card) else: @@ -620,7 +745,15 @@ def _startingLeft(self, card): return tot + tod*1000 def _leftToday(self, delays, left, now=None): - "The number of steps that can be completed by the day cutoff." + """The number of the last ```left``` steps that can be completed + before the day cutoff. Assuming the first step is done + ```now```. + + delays -- the list of delays + left -- the number of step to consider (at the end of the + list) + now -- the time at which the first step is done. + """ if not now: now = intTime() delays = delays[-left:] @@ -633,6 +766,19 @@ def _leftToday(self, delays, left, now=None): return ok+1 def _graduatingIvl(self, card, conf, early, adj=True): + """ + The interval before the next review. + If its lapsed card in a filtered deck, then use _dynIvlBoost. + Otherwise, if button was easy or not, use the 'ints' value + according to conf parameter. + Maybe apply some fuzzyness according to adj + + Card is supposed to be in learning. + card -- a card object + conf -- the configuration dictionnary for this kind of card + early -- whether "easy" was pressed + adj -- whether to add fuzziness + """ if card.type == CARD_DUE: # lapsed card being relearnt if card.odid: @@ -651,7 +797,16 @@ def _graduatingIvl(self, card, conf, early, adj=True): return ideal def _rescheduleNew(self, card, conf, early): - "Reschedule a new card that's graduated for the first time." + """Reschedule a new card that's graduated for the first time. + + Set its factor according to conf. + Set its interval. If it's lapsed in dynamic deck, use + _dynIvlBoost. + Otherwise, the interval is found in conf['ints'][1 if early + else 0]. + Change due date according to the interval. + Put initial factor. + """ card.ivl = self._graduatingIvl(card, conf, early) card.due = self.today+card.ivl card.factor = conf['initialFactor'] @@ -691,6 +846,7 @@ def removeLrn(self, ids=None): f"select id from cards where queue in ({QUEUE_LRN}, {QUEUE_DAY_LRN}) %s" % extra)) def _lrnForDeck(self, did): + """Number of review of cards in learing of deck did. """ cnt = self.col.db.scalar( f""" select sum(left/1000) from @@ -710,12 +866,20 @@ def _deckRevLimit(self, did): return self._deckNewLimit(did, self._deckRevLimitSingle) def _deckRevLimitSingle(self, d): + """Maximum number of card to review today in deck d. + + self.reportLimit for dynamic deck. Otherwise the number of review according to deck option, plus the number of review added in custom study today. + keyword arguments: + d -- a deck object""" if d['dyn']: return self.reportLimit c = self.col.decks.confForDid(d['id']) return max(0, c['rev']['perDay'] - d['revToday'][1]) def _revForDeck(self, did, lim): + """number of cards to review today for deck did + + Minimum between this number, self report and limit. Not taking subdeck into account """ lim = min(lim, self.reportLimit) return self.col.db.scalar( f""" @@ -725,7 +889,9 @@ def _revForDeck(self, did, lim): did, self.today, lim) def _resetRevCount(self): + """Set revCount""" def cntFn(did, lim): + """Number of review cards to see today for deck with id did. At most equal to lim.""" return self.col.db.scalar(f""" select count() from (select id from cards where did = ? and queue = {QUEUE_REV} and due <= ? limit %d)""" % (lim), @@ -734,6 +900,7 @@ def cntFn(did, lim): self._deckRevLimitSingle, cntFn) def _resetRev(self): + """Set revCount, empty _revQueue, _revDids""" self._resetRevCount() self._revQueue = [] self._revDids = self.col.decks.active()[:] @@ -791,6 +958,18 @@ def totalRevForCurrentDeck(self): ########################################################################## def _answerRevCard(self, card, ease): + """ + move the card out of filtered if required + change the queue + + if not (filtered without rescheduling): + change interval + change due + change factor + change lapse if BUTTON_ONE + + log this review + """ delay = 0 if ease == BUTTON_ONE: delay = self._rescheduleLapse(card) @@ -799,6 +978,25 @@ def _answerRevCard(self, card, ease): self._logRev(card, ease, delay) def _rescheduleLapse(self, card): + """ + The number of second for the delay until the next time the card can + be reviewed. 0 if it should not be reviewed (because leech, or + because conf['delays'] is empty) + + Called the first time we press "again" on a review card. + + + Unless filtered without reschedule: + lapse incread + ivl changed + factor decreased by 200 + due changed. + + if leech, suspend and return 0. + + card -- review card + """ + conf = self._lapseConf(card) card.lastIvl = card.ivl if self._resched(card): @@ -812,15 +1010,16 @@ def _rescheduleLapse(self, card): # if suspended as a leech, nothing to do delay = 0 if self._checkLeech(card, conf) and card.queue == QUEUE_SUSPENDED: - return delay + return delay#i.e. return 0 # if no relearning steps, nothing to do if not conf['delays']: - return delay + return delay#i.e. return 0 # record rev due date for later if not card.odue: card.odue = card.due delay = self._delayForGrade(conf, 0) card.due = int(delay + time.time()) + #number of rev+1000*number of rev to do today card.left = self._startingLeft(card) # queue LRN if card.due < self.dayCutoff: @@ -838,6 +1037,18 @@ def _nextLapseIvl(self, card, conf): return max(conf['minInt'], int(card.ivl*conf['mult'])) def _rescheduleRev(self, card, ease): + """Update the card according to review. + + If it's filtered, remove from filtered. If the filtered deck + states not to update, nothing else is done. + + Change the interval. Change the due date. Change the factor. + + + ease -- button pressed. Between 2 and 4 + card -- assumed to be in review mode + + """ # update interval card.lastIvl = card.ivl if self._resched(card): @@ -853,6 +1064,11 @@ def _rescheduleRev(self, card, ease): card.odue = 0 def _logRev(self, card, ease, delay): + """Log this review. Retry once if failed. + card -- a card object + ease -- the button pressed + delay -- if the answer is again, then the number of second until the next review + """ def log(): self.col.db.execute( "insert into revlog values (?,?,?,?,?,?,?,?,?)", @@ -870,7 +1086,10 @@ def log(): ########################################################################## def _nextRevIvl(self, card, ease): - "Ideal next interval for CARD, given EASE." + """Ideal next interval for CARD, given EASE. + + Fuzzyness not applied. + Cardds assumed to be a review card succesfully reviewed.""" delay = self._daysLate(card) conf = self._revConf(card) fct = card.factor / 1000 @@ -888,10 +1107,24 @@ def _nextRevIvl(self, card, ease): return min(interval, conf['maxIvl']) def _fuzzedIvl(self, ivl): + """Return a randomly chosen number of day for the interval, + not far from ivl. + + See ../documentation/computing_intervals for a clearer version + of this documentation + """ min, max = self._fuzzIvlRange(ivl) return random.randint(min, max) def _fuzzIvlRange(self, ivl): + """Return an increasing pair of numbers. The new interval will be a + number randomly selected between the first and the second + element. + + See ../documentation/computing_intervals for a clearer version + of this documentation + + """ if ivl < 2: return [1, 1] elif ivl == 2: @@ -907,7 +1140,9 @@ def _fuzzIvlRange(self, ivl): return [ivl-fuzz, ivl+fuzz] def _constrainedIvl(self, ivl, conf, prev): - "Integer interval after interval factor and prev+1 constraints applied." + """A new interval. Ivl multiplie by the interval + factor of this conf. Greater than prev. + """ new = ivl * conf.get('ivlFct', 1) return int(max(new, prev+1)) @@ -917,11 +1152,24 @@ def _daysLate(self, card): return max(0, self.today - due) def _updateRevIvl(self, card, ease): + """ Compute the next interval, fuzzy it, ensure ivl increase + and is at most maxIvl. + + Card is assumed to be a review card.""" idealIvl = self._nextRevIvl(card, ease) - card.ivl = min(max(self._adjRevIvl(card, idealIvl), card.ivl+1), - self._revConf(card)['maxIvl']) + fuzzedIvl = self._adjRevIvl(card, idealIvl) + increasedIvl = max(fuzzedIvl, card.ivl+1) + card.ivl = min(increasedIvl, self._revConf(card)['maxIvl']) def _adjRevIvl(self, card, idealIvl): + """Return an interval, similar to idealIvl, but randomized. + See ../documentation/computing_intervals for the rule to + generate this fuzziness. + + if _speadRev is to False (by an Add-on, I guess), it returns + the input exactly. + + card is not used.""" if self._spreadRev: idealIvl = self._fuzzedIvl(idealIvl) return idealIvl @@ -960,6 +1208,13 @@ def _fillDyn(self, deck): return ids def emptyDyn(self, did, lim=None): + """Moves cram cards to their deck + Cards in learning mode move to their previous type. + + Keyword arguments: + lim -- the query which decides which cards are used + did -- assuming lim is not provided/false, the (filtered) deck concerned by this call + """ if not lim: lim = "did = %s" % did self.col.log(self.col.db.list("select id from cards where %s" % lim)) @@ -1017,9 +1272,20 @@ def _moveToDyn(self, did, ids): did = ?, queue = %s, due = ?, usn = ? where id = ?""" % queue, data) def _dynIvlBoost(self, card): + """New interval for a review card in a dynamic interval. + + Maximum between old interval and + elapsed*((card.factor/1000)+1.2)/2, with elapsed being the + time between today and the last review. + + This number is constrained to be between 1 and the parameter + ```maxIvl``` of the card's configuration. + + """ assert card.odid and card.type == CARD_DUE assert card.factor - elapsed = card.ivl - (card.odue - self.today) + lastReview = (card.odue - card.ivl) + elapsed = self.today - lastReview factor = ((card.factor/1000)+1.2)/2 ivl = int(max(card.ivl, elapsed * factor, 1)) conf = self._revConf(card) @@ -1058,9 +1324,15 @@ def _checkLeech(self, card, conf): ########################################################################## def _cardConf(self, card): + """The configuration of this card's deck. See decks.py + documentation to read more about them.""" return self.col.decks.confForDid(card.did) def _newConf(self, card): + """The configuration for "new" of this card's deck.See decks.py + documentation to read more about them. + + """ conf = self._cardConf(card) # normal deck if not card.odid: @@ -1081,6 +1353,10 @@ def _newConf(self, card): ) def _lapseConf(self, card): + """The configuration for "lapse" of this card's deck.See decks.py + documentation to read more about them. + + """ conf = self._cardConf(card) # normal deck if not card.odid: @@ -1100,6 +1376,10 @@ def _lapseConf(self, card): ) def _revConf(self, card): + """The configuration for "review" of this card's deck.See decks.py + documentation to read more about them. + + """ conf = self._cardConf(card) # normal deck if not card.odid: @@ -1108,9 +1388,13 @@ def _revConf(self, card): return self.col.decks.confForDid(card.odid)['rev'] def _deckLimit(self): + """The list of active decks, as comma separated parenthesized + string""" return ids2str(self.col.decks.active()) def _resched(self, card): + """Whether this review must be taken into account when this + card to reschedule the card""" conf = self._cardConf(card) if not conf['dyn']: return True @@ -1120,6 +1404,13 @@ def _resched(self, card): ########################################################################## def _updateCutoff(self): + """ + For each deck, set its entry *Today's first element to today's date + if it's a new day: + - log the new day + - unbury new cards + - change today and daycutoff + """ oldToday = self.today # days since col created self.today = int((time.time() - self.col.crt) // 86400) @@ -1372,6 +1663,20 @@ def resetCards(self, ids): ########################################################################## def sortCards(self, cids, start=1, step=1, shuffle=False, shift=False): + """Change the due of new cards in `cids`. + + Each card of the same note have the same due. The order of the + due is random if shuffle. Otherwise the order of the note `n` is + similar to the order of the first occurrence of a card of `n` in cids. + + Keyword arguments: + cids -- list of card ids to reorder (i.e. change due). Not new cards are ignored + start -- the first due to use + step -- the difference between to successive due of notes + shuffle -- whether to shuffle the note. By default, the order is similar to the created order + shift -- whether to change the due of all new cards whose due is greater than start (to ensure that the new due of cards in cids is not already used) + + """ scids = ids2str(cids) now = intTime() nids = [] @@ -1391,7 +1696,7 @@ def sortCards(self, cids, start=1, step=1, shuffle=False, shift=False): for c, nid in enumerate(nids): due[nid] = start+c*step # pylint: disable=undefined-loop-variable - high = start+c*step + high = start+c*step #Highest due which will be used # shift? if shift: low = self.col.db.scalar( @@ -1412,14 +1717,27 @@ def sortCards(self, cids, start=1, step=1, shuffle=False, shift=False): "update cards set due=:due,mod=:now,usn=:usn where id = :cid", d) def randomizeCards(self, did): + """Change the due value of new cards of deck `did`. The new due value + is the same for every card of a note (as long as they are in the same + deck.)""" cids = self.col.db.list("select id from cards where did = ?", did) self.sortCards(cids, shuffle=True) def orderCards(self, did): + """Change the due value of new cards of deck `did`. The new due value + is the same for every card of a note (as long as they are in the + same deck.) + + The note are now ordered according to the smallest id of their + cards. It generally means they are ordered according to date + creation. + """ cids = self.col.db.list("select id from cards where did = ? order by id", did) self.sortCards(cids) def resortConf(self, conf): + """When a deck configuration's order of new card is changed, apply + this change to each deck having the same deck configuration.""" for did in self.col.decks.didsForConf(conf): if conf['new']['order'] == NEW_CARDS_RANDOM: self.randomizeCards(did) diff --git a/anki/sound.py b/anki/sound.py index d41ea8f8e5b..dc220c63383 100644 --- a/anki/sound.py +++ b/anki/sound.py @@ -12,21 +12,28 @@ # Shared utils ########################################################################## +""" +Regexp matching [sound:...] in field. It's only match is the file name +""" _soundReg = r"\[sound:(.*?)\]" def playFromText(text): + """Play in order all sound matched in text.""" for match in allSounds(text): # filename is html encoded match = html.unescape(match) play(match) def allSounds(text): + """The list of match of sounds in text""" return re.findall(_soundReg, text) def stripSounds(text): + """The text without its sound field.""" return re.sub(_soundReg, "", text) def hasSound(text): + """Whether field text contains some [sound:...].""" return re.search(_soundReg, text) is not None # Packaged commands @@ -180,7 +187,9 @@ def cleanupOldMplayerProcesses(): mplayerQueue = [] mplayerManager = None mplayerReader = None + mplayerEvt = threading.Event() +"""Whether the queue of things to play should be cleared""" mplayerClear = False class MplayerMonitor(threading.Thread): diff --git a/anki/storage.py b/anki/storage.py index 6375d6f1c53..e8ecf970b30 100644 --- a/anki/storage.py +++ b/anki/storage.py @@ -16,7 +16,11 @@ addForwardOptionalReverse, addBasicTypingModel def Collection(path, lock=True, server=False, log=False): - "Open a new or existing collection. Path must be unicode." + """Open a new or existing collection. Path must be unicode. + + server -- always False in anki without add-on. + log -- Boolean stating whether log must be made in the file, with same name than the collection, but ending in .log. + """ assert path.endswith(".anki2") path = os.path.abspath(path) create = not os.path.exists(path) diff --git a/anki/sync.py b/anki/sync.py index f5d21e89dc5..590afc6c560 100644 --- a/anki/sync.py +++ b/anki/sync.py @@ -2,6 +2,9 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +# r means server +# l means collection + import io import gzip import random @@ -27,12 +30,43 @@ class Syncer: + """The hook "sync" takes as parameter a string describing what is + going to be done. It seems that, by default, those hook are empty. + + lmod -- last modified of the collection in milliseconds + rmod -- last modified of the collection in milliseconds + minUsn -- USN of the collection + col -- its collection + server -- the server. Object of class RemoteServer in pratice + syncMsg -- By default the empty string. Otherwise, a message from meta. + uname -- username (login/email) + tablesLeft -- A subset of ["revlog", "cards", "notes"], stating which tables have not yet be synchronized. + cursor -- A cursor to the table revlog, cards, or notes, allowing to find elements not yet received from the database. + + """ + def __init__(self, col, server=None): + """Save in the Syncer the value of the two parameters. """ + #It is not clear what is done if server is None. But it never occurs in this code self.col = col self.server = server def sync(self): - "Returns 'noChanges', 'fullSync', 'success', etc" + """ + Possible return values: + * badAuth: wrong login or password + * clockOff: there is more than 5 minutes of difference between + the clock here and on server. Thus synchronization rejected. + * noChanges: mod's value are the same in collection and + server. It means that not a single change occured, thus there + are nothing to synchronize. + * fullSync: the schema differ in collection and on server. A + full sync is thus required. + * basicCheckFailed: if the basic check fails + * sanityCheckFailed: if the sanity check fails according to + sync. + * success: everything went all right. + """ self.syncMsg = "" self.uname = "" # if the deck has any pending changes, flush them first and bump mod @@ -41,40 +75,40 @@ def sync(self): # step 1: login & metadata runHook("sync", "login") - meta = self.server.meta() - self.col.log("rmeta", meta) - if not meta: + serverMeta = self.server.meta() + self.col.log("rmeta", serverMeta) + if not serverMeta: return "badAuth" # server requested abort? - self.syncMsg = meta['msg'] - if not meta['cont']: + self.syncMsg = serverMeta['msg'] + if not serverMeta['cont']: return "serverAbort" else: # don't abort, but if 'msg' is not blank, gui should show 'msg' # after sync finishes and wait for confirmation before hiding pass - rscm = meta['scm'] - rts = meta['ts'] - self.rmod = meta['mod'] - self.maxUsn = meta['usn'] - self.uname = meta.get("uname", "") - self.hostNum = meta.get("hostNum") - meta = self.meta() - self.col.log("lmeta", meta) - self.lmod = meta['mod'] - self.minUsn = meta['usn'] - lscm = meta['scm'] - lts = meta['ts'] - if abs(rts - lts) > 300: + serverSchemaMod = serverMeta['scm'] + serverTimeStamp = serverMeta['ts'] + self.ServerMod = serverMeta['mod'] + self.maxUsn = serverMeta['usn'] + self.uname = serverMeta.get("uname", "") + self.hostNum = serverMeta.get("hostNum") + computerMeta = self.meta() + self.col.log("lmeta", computerMeta) + self.computerMod = computerMeta['mod'] + self.minUsn = computerMeta['usn'] + computerSchemaMod = computerMeta['scm'] + computerTimeStamp = computerMeta['ts'] + if abs(serverTimeStamp - computerTimeStamp) > 300: self.col.log("clock off") return "clockOff" - if self.lmod == self.rmod: + if self.computerMod == self.ServerMod: self.col.log("no changes") return "noChanges" - elif lscm != rscm: + elif computerSchemaMod != serverSchemaMod: self.col.log("schema diff") return "fullSync" - self.lnewer = self.lmod > self.rmod + self.lnewer = self.computerMod > self.ServerMod # step 1.5: check collection is valid if not self.col.basicCheck(): self.col.log("basic check") @@ -94,10 +128,11 @@ def sync(self): # ...and small objects lchg = self.changes() - rchg = self.server.applyChanges(changes=lchg) - self.mergeChanges(lchg, rchg) + serverChange = self.server.applyChanges(changes=lchg) + self.mergeChanges(lchg, serverChange) # step 3: stream large tables from server runHook("sync", "server") + ################ done while 1: runHook("sync", "stream") chunk = self.server.chunk() @@ -131,6 +166,7 @@ def sync(self): return "success" def _gravesChunk(self, graves): + """A pair. If graves contains less than 250 elements, then grave,None. Else the 250 first elements of graves, and the remaining elements)""" lim = 250 chunk = dict(notes=[], cards=[], decks=[]) for cat in "notes", "cards", "decks": @@ -145,6 +181,11 @@ def _gravesChunk(self, graves): return chunk, None def meta(self): + """A dictionnary with: + -mod, scm, usn according to col's data + -ts the actual time stamp + -musn, msg and cont, initialized to some default constant + """ return dict( mod=self.col.mod, scm=self.col.scm, @@ -156,7 +197,10 @@ def meta(self): ) def changes(self): - "Bundle up small objects." + """Bundle up small objects. + + A dict with models, decks, tags. If mod is newer here than on server, then conf and crt. + """ d = dict(models=self.getModels(), decks=self.getDecks(), tags=self.getTags()) @@ -165,19 +209,41 @@ def changes(self): d['crt'] = self.col.crt return d - def mergeChanges(self, lchg, rchg): + def mergeChanges(self, lchg, serverChange): + """ + serverChange -- + """ # then the other objects - self.mergeModels(rchg['models']) - self.mergeDecks(rchg['decks']) - self.mergeTags(rchg['tags']) - if 'conf' in rchg: - self.mergeConf(rchg['conf']) - # this was left out of earlier betas - if 'crt' in rchg: - self.col.crt = rchg['crt'] + self.mergeModels(serverChange['models']) + self.mergeDecks(serverChange['decks']) + self.mergeTags(serverChange['tags']) + if 'conf' in serverChange: + self.mergeConf(serverChange['conf']) + if 'crt' in serverChange: + self.col.crt = serverChange['crt'] self.prepareToChunk() def sanityCheck(self): + """Check whether the synchronization went well. + + USN should not be -1 in cards, notes, revlog, graves, deck, + tag, model. If it is the case, return a string giving the name of the + table not satisfying it. + + If a non-root deck has no parent, it is created. + + Returns: + [three numbers to show in anki + list/footer. I.e. Number of new cards, learning repetition, + review card. (for selected deck), + number of card, + number of notes, + number of revlog, + number of grave, + number of models, + numbel of decks, + number of deck's options.] + """ if not self.col.basicCheck(): return "failed basic check" for t in "cards", "notes", "revlog", "graves": @@ -230,7 +296,11 @@ def prepareToChunk(self): self.cursor = None def cursorForTable(self, table): - lim = self.usnLim() + """A cursor returning the entire line from the table argument, where usn is -1, and replaced by maxUsn. + + The content of the table is not changed however. + """ + lim = self.usnLim() # "usn = -1" x = self.col.db.execute d = (self.maxUsn, lim) if table == "revlog": @@ -247,6 +317,15 @@ def cursorForTable(self, table): from notes where %s""" % d) def chunk(self): + """A dictionnary containing keys K in 'revlog', 'cards', 'notes' whose + usn value is -1. Each entry is an entire line of the table K, + except that usn is replaced by maxUsn. + + It also contains 'done', a Boolean stating which states whether every lines are treated. + + If the table is empty, usn is changed from -1 to maxUsn and + its name is removed from self.tablesLeft. + """ buf = dict(done=False) lim = 250 while self.tablesLeft and lim: @@ -270,6 +349,14 @@ def chunk(self): return buf def applyChunk(self, chunk): + """ + Everything in chunk is added to the collection, unless it is + already in and modified later in the collection than on the server. + + chunk -- a dict containing some key from 'revlog', 'cards', and + 'notes'. Each of their values are a line (log, card or note), + which is new on the server. + """ if "revlog" in chunk: self.mergeRevlog(chunk['revlog']) if "cards" in chunk: @@ -281,6 +368,12 @@ def applyChunk(self, chunk): ########################################################################## def removed(self): + """A dict associating to 'cards', 'notes' and 'decks' the list of ids + of such object deleted sync last synchronization. Change the + usn value of those graves element to indicate that they have + been deleted. + + """ cards = [] notes = [] decks = [] @@ -302,6 +395,11 @@ def removed(self): return dict(cards=cards, notes=notes, decks=decks) def remove(self, graves): + """Remove the notes, cards and decks whose id are in graves['notes'], + graves['cards'] and graves['decks']. Don't remove child of + deleted deck, nor its card if any remain. + + """ # pretend to be the server so we don't set usn = -1 self.col.server = True @@ -319,23 +417,37 @@ def remove(self, graves): ########################################################################## def getModels(self): + """ + The list of models whose usn is -1. I.e. the ones which have been created/changed since last sync. + Their usn is then changed no maxUsn. + """ mods = [m for m in self.col.models.all() if m['usn'] == -1] for m in mods: m['usn'] = self.maxUsn self.col.models.save() return mods - def mergeModels(self, rchg): - for r in rchg: - l = self.col.models.get(r['id']) + def mergeModels(self, serverChange): + """ + serverChange -- a list of model (note type) object. They are assumed to have been changed on server since last sync. + + Each note type whose mod time is greater on server, or which does not exists in collection, is copied into the collection. + """ + for modelFromServer in serverChange: + modelVersionFromCollection = self.col.models.get(modelFromServer['id']) # if missing locally or server is newer, update - if not l or r['mod'] > l['mod']: - self.col.models.update(r) + if not modelVersionFromCollection or modelFromServer['mod'] > modelVersionFromCollection['mod']: + self.col.models.update(modelFromServer) # Decks ########################################################################## def getDecks(self): + """A list of size two, with decks and deck's configuration (option), + with usn equal to -1. I.e. modified since last sync. Their usn + is changed to maxUsn, i.e. the one currently considered. + + """ decks = [g for g in self.col.decks.all() if g['usn'] == -1] for g in decks: g['usn'] = self.maxUsn @@ -345,29 +457,34 @@ def getDecks(self): self.col.decks.save() return [decks, dconf] - def mergeDecks(self, rchg): - for r in rchg[0]: - l = self.col.decks.get(r['id'], False) + def mergeDecks(self, serverChange): + for deckFromServer in serverChange[0]: + deckVersionFromCollection = self.col.decks.get(deckFromServer['id'], False) # work around mod time being stored as string - if l and not isinstance(l['mod'], int): - l['mod'] = int(l['mod']) + if deckVersionFromCollection and not isinstance(deckVersionFromCollection['mod'], int): + deckVersionFromCollection['mod'] = int(deckVersionFromCollection['mod']) # if missing locally or server is newer, update - if not l or r['mod'] > l['mod']: - self.col.decks.update(r) - for r in rchg[1]: + if not deckVersionFromCollection or deckFromServer['mod'] > deckVersionFromCollection['mod']: + self.col.decks.update(deckFromServer) + for deckFromServer in serverChange[1]: try: - l = self.col.decks.getConf(r['id']) + deckVersionFromCollection = self.col.decks.getConf(deckFromServer['id']) except KeyError: - l = None + deckVersionFromCollection = None # if missing locally or server is newer, update - if not l or r['mod'] > l['mod']: - self.col.decks.updateConf(r) + if not deckVersionFromCollection or deckFromServer['mod'] > deckVersionFromCollection['mod']: + self.col.decks.updateConf(deckFromServer) # Tags ########################################################################## def getTags(self): + """A list of tags with usn equal to -1. I.e. modified since last + sync. Their usn is changed to maxUsn, i.e. the one currently + considered. + + """ tags = [] for t, usn in self.col.tags.allItems(): if usn == -1: @@ -377,37 +494,74 @@ def getTags(self): return tags def mergeTags(self, tags): + """Given a list/set of tags, add any tag missing in the registry to + the registry. If there is such a new tag, call the hook + newTag. + + """ self.col.tags.register(tags, usn=self.maxUsn) # Cards/notes/revlog ########################################################################## def mergeRevlog(self, logs): + """Each log from logs is added to the table revlog, unless a rev with + the same id is already there. (The id is unique with extremly + high probability, thus it would be the same review). + + """ self.col.db.executemany( "insert or ignore into revlog values (?,?,?,?,?,?,?,?,?)", logs) def newerRows(self, data, table, modIdx): + """ + The subset of `data` which either are not in the collection + (according to their id), or such that the last modification in + the server is greater than in the collection. + + data -- a container of tuple, each representing an entry of + the table 'table' from the server, i.e. to be updated or + created. + table -- 'card' or 'note', name of a table + modIdx -- index of mod in the tuple + """ ids = (r[0] for r in data) - lmods = {} + lmods = {} # subset of (id,mod) of data's id in which usn is -1. for id, mod in self.col.db.execute( "select id, mod from %s where id in %s and %s" % ( table, ids2str(ids), self.usnLim())): lmods[id] = mod - update = [] - for r in data: - if r[0] not in lmods or lmods[r[0]] < r[modIdx]: - update.append(r) + update = [] # lines from server (data), which either are not + # in the collection, or such that the mod time is greater on + # the server than in the collection. + for entryFromServer in data: + if entryFromServer[0] not in lmods or lmods[entryFromServer[0]] < entryFromServer[modIdx]: + update.append(entryFromServer) self.col.log(table, data) return update def mergeCards(self, cards): + """Each card from cards is added to the table cards, unless a card + with the same id is already there and has a newer mod + time. (The id is unique with extremly high probability, thus + it would be the same review). + + """ + rows = self.newerRows(cards, "cards", 4) self.col.db.executemany( "insert or replace into cards values " "(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", - self.newerRows(cards, "cards", 4)) + rows) def mergeNotes(self, notes): + """Each note from notes is added to the table notes, unless a note + with the same id is already there and has a newer mod + time. (The id is unique with extremly high probability, thus + it would be the same review). + + The field cache of those notes are then modified. + """ rows = self.newerRows(notes, "notes", 3) self.col.db.executemany( "insert or replace into notes values (?,?,?,?,?,?,?,?,?,?,?)", @@ -418,35 +572,56 @@ def mergeNotes(self, notes): ########################################################################## def getConf(self): + """The conf of the collection.""" return self.col.conf def mergeConf(self, conf): + """Change collection's conf to the argument""" self.col.conf = conf # Wrapper for requests that tracks upload/download progress ########################################################################## class AnkiRequestsClient: - + """ + session -- A request session object + verify -- Whether to verify the certificate. By default true unless the os ANKI_NOVERIFYSSL is set to truthy. + timeout -- as for requests + """ verify = True timeout = 60 - def __init__(self): self.session = requests.Session() def post(self, url, data, headers): + """ + Similar to a post request, where: + * User-Agent name is added + * hook httpSend is run + * stream is true, timeout and verify are as in the class. + """ data = _MonitoringFile(data) headers['User-Agent'] = self._agentName() return self.session.post( url, data=data, headers=headers, stream=True, timeout=self.timeout, verify=self.verify) def get(self, url, headers=None): + """ + Similar to a post request, where: + * User-Agent name is added to header + * stream is true, timeout and verify are as in the class. + """ if headers is None: headers = {} headers['User-Agent'] = self._agentName() return self.session.get(url, stream=True, headers=headers, timeout=self.timeout, verify=self.verify) def streamContent(self, resp): + """ + Return a string containing the content of the entire response to a request. + + If the response is not successful, raise requests.exceptions.HTTPError + """ resp.raise_for_status() buf = io.BytesIO() @@ -456,6 +631,7 @@ def streamContent(self, resp): return buf.getvalue() def _agentName(self): + """Anki versionNumber""" from anki import version return "Anki {}".format(version) @@ -468,6 +644,7 @@ def _agentName(self): class _MonitoringFile(io.BufferedReader): def read(self, size=-1): + """"Similar to io.BufferedReader's read method, where the hook httpSend is emitted after data is found.""" data = io.BufferedReader.read(self, HTTP_BUF_SIZE) runHook("httpSend", len(data)) return data @@ -476,7 +653,14 @@ def read(self, size=-1): ########################################################################## class HttpSyncer: - + """ + skey -- a random 8 hexadecimal value + hostNum -- the value of hostNum in the profile (i.e. a number which states the number of the ankiweb server which is used to synchronize) + hkey -- the value of SyncKey in the profile (i.e. a random string replacing the password) + postVars -- dictionnary to use in the post request. + prefix -- main folder in anki server to use for the synchrozination. By default sync, except for media where it is msync + client -- a client, allowing at least to post() and streamContent. By default AnkiRequestsClient + """ def __init__(self, hkey=None, client=None, hostNum=None): self.hkey = hkey self.skey = checksum(str(random.random()))[:8] @@ -486,6 +670,11 @@ def __init__(self, hkey=None, client=None, hostNum=None): self.prefix = "sync/" def syncURL(self): + """ + Start of the url to use for synchrozination. Some other subfolder/file may be staten after. + + It depends on whether we are in devmode, and of the hostNum value.s + """ if devMode: url = "https://l1sync.ankiweb.net/" else: @@ -493,6 +682,7 @@ def syncURL(self): return url + self.prefix def assertOk(self, resp): + """Raise an exception unless status code of resp is 200""" # not using raise_for_status() as aqt expects this error msg if resp.status_code != 200: raise Exception("Unknown response code: %s" % resp.status_code) @@ -504,6 +694,31 @@ def assertOk(self, resp): # support file uploading, so this is the more compatible choice. def _buildPostData(self, fobj, comp): + """ + A pair (headers, buffer) as follow. + + buffer's position is 0. It contains, separated by "--Anki-sync-boundary", +«Content-Disposition: form-data; name="{key}" + +value +» + where key,values come from self.postVars, and also contains key 'c', whose value is 1 if it's compressing, 0 otherwise. + +If there is an object, then it also contains +« +Content-Disposition: form-data; name="data"; filename="data"\r\n\ +Content-Type: application/octet-stream\r\n\r\n"» +{object} +» + In any case, it ends with +«-- +» + the header is a dict with 'Content-Type' and 'Content-Length'. + + + comp -- whether to compress. If truthy, it's the compresslevel passed to gzip. + fobj -- an object which can be read. + """ BOUNDARY=b"Anki-sync-boundary" bdry = b"--"+BOUNDARY buf = io.BytesIO() @@ -551,6 +766,18 @@ def _buildPostData(self, fobj, comp): return headers, buf def req(self, method, fobj=None, comp=6, badAuthRaises=True): + """ + The answer to a post request, to /method, whose body comes from self.postVars, compression and potentially the object fobj. + + method -- the «file», i.e. last part of the URL. It states which can of data we send/request. It may be: + * hostKey, meta for initialization of sync + * applyGraves, applyChanges, start, chunk, applyChunk, sanityCheck2, finish, abort for normal syn + * download, upload for full sync + * begin, mediaChanges, downloadFiles, uploadChanges, mediaSanity, newMediaTest for media + fobj -- An object which can be read and must be send + comp -- Level of compression to use for gzip. + badAuthRaises -- whether to accept 403 status without raising error. Instead return False. + """ headers, body = self._buildPostData(fobj, comp) r = self.client.post(self.syncURL()+method, data=body, headers=headers) @@ -582,13 +809,17 @@ def hostKey(self, user, pw): return self.hkey def meta(self): + """ Ask the server for an object which should contain + + + """ self.postVars = dict( k=self.hkey, s=self.skey, ) + d = dict(v=SYNC_VER, cv="ankidesktop,%s,%s"%(versionWithBuild(), platDesc())) ret = self.req( - "meta", io.BytesIO(json.dumps(dict( - v=SYNC_VER, cv="ankidesktop,%s,%s"%(versionWithBuild(), platDesc()))).encode("utf8")), + "meta", io.BytesIO(json.dumps(d).encode("utf8")), badAuthRaises=False) if not ret: # invalid auth @@ -599,6 +830,7 @@ def applyGraves(self, **kw): return self._run("applyGraves", kw) def applyChanges(self, **kw): + """Send every small change to the server.""" return self._run("applyChanges", kw) def start(self, **kw): @@ -637,7 +869,18 @@ def __init__(self, col, hkey, client, hostNum): self.col = col def download(self): + """Download a database from the server. If instead it receives + "upgradeRequired", a message stating to go on ankiweb is + shown. Otherwise, if the downloaded db has no card while + current collection has card, it returns + "downloadClobber". Otherwise, the downloaded database replace + the collection's database. + + It also change message to "Downloading for AnkiWeb...". + + """ runHook("sync", "download") + #whether the collection has at least one card. localNotEmpty = self.col.db.scalar("select 1 from cards") self.col.close() cont = self.req("download") @@ -687,10 +930,26 @@ def upload(self): class MediaSyncer: def __init__(self, col, server=None): + """Save in the Syncer the value of the two parameters""" self.col = col self.server = server def sync(self): + """ + Return either: + * "corruptMediaDB": if reading the database raise an exception + * "noChanges": if the usn on collection and server are the same + * "OK": if everything was correctly downloaded and uploaded + * Any other message sent by the server during mediaSanity check. + + It sends the following messages: + * begin: send hostkey and "ankidesktop,{anki's version number},{platform}:{platform's + version}". Returing an id to use until the end of communication + * mediaChanges: the name of new/modified medias, and usn in collection. Return name of new/modified media on server + * downloadFiles: request files named by the server when necessary, receive zip files with up to 25 files, and save them in the collection. + * uploadChanges: Send zip files with up to 25 files. + * mediaSanity: sending the number of media to server, to check whether there is the same number online. If not, empty media database and return value sent by the server. + """ # check if there have been any changes runHook("sync", "findMedia") self.col.log("findChanges") @@ -828,6 +1087,18 @@ def __init__(self, col, hkey, client, hostNum): self.prefix = "msync/" def begin(self): + """Send a request to initialize the communication. It contains: + * 'k': the hostkey (a number sent during synchronization of data, to identify the user) + * 'v': "ankidesktop,{anki's version number},{platform}:{platform's + version}", with platform being either win, lin, mac or unknown. In + the last case, there are no version sent. + + Set the identification number as provided by the server. This + number is used for this communication only. + + Raise an exception if the response contains an error field or is not json. + + """ self.postVars = dict( k=self.hkey, v="ankidesktop,%s,%s"%(anki.version, platDesc()) @@ -860,6 +1131,11 @@ def mediaSanity(self, **kw): self.req("mediaSanity", io.BytesIO(json.dumps(kw).encode("utf8")))) def _dataOnly(self, resp): + """ + If the error in resp is truthy, log it, raise an exception. Otherwise, return the data of resp. + + resp -- a json utf8 string. Otherwise raise an exception. Received from the server + """ resp = json.loads(resp.decode("utf8")) if resp['err']: self.col.log("error returned:%s"%resp['err']) diff --git a/anki/tags.py b/anki/tags.py index f9e1860ba6c..fc2f9c33f13 100644 --- a/anki/tags.py +++ b/anki/tags.py @@ -37,7 +37,11 @@ def flush(self): ############################################################# def register(self, tags, usn=None): - "Given a list of tags, add any missing ones to tag registry." + """Given a list/set of tags, add any tag missing in the registry to + the registry. If there is such a new tag, call the hook + newTag. + + """ found = False for t in tags: if t not in self.tags: @@ -85,7 +89,13 @@ def byDeck(self, did, children=False): ############################################################# def bulkAdd(self, ids, tags, add=True): - "Add tags in bulk. TAGS is space-separated." + """Add tags in bulk. TAGS is space-separated. + + keyword arguments + ids -- a list of id + tags -- a string of space-separated tag + add -- whether to add (True) or to remove (False) + """ newTags = self.split(tags) if not newTags: return @@ -102,8 +112,8 @@ def bulkAdd(self, ids, tags, add=True): lim = " or ".join( [l+"like :_%d" % c for c, t in enumerate(newTags)]) res = self.col.db.all( - "select id, tags from notes where id in %s and (%s)" % ( - ids2str(ids), lim), + f"""select id, tags from notes where id in + {ids2str(ids)} and ({lim})""" , **dict([("_%d" % x, '%% %s %%' % y.replace('*', '%')) for x, y in enumerate(newTags)])) # update tags @@ -127,7 +137,8 @@ def split(self, tags): return [t for t in tags.replace('\u3000', ' ').split(" ") if t] def join(self, tags): - "Join tags into a single string, with leading and trailing spaces." + """Join tags into a single string, separated by whitespace, with leading +and trailing spaces.""" if not tags: return "" return " %s " % " ".join(tags) @@ -172,7 +183,7 @@ def canonify(self, tagList): return sorted(set(strippedTags)) def inList(self, tag, tags): - "True if TAG is in TAGS. Ignore case." + """Whether tag is in tags. Ignore case.""" return tag.lower() in [t.lower() for t in tags] # Sync handling diff --git a/anki/template/LICENSE b/anki/template/LICENSE old mode 100644 new mode 100755 diff --git a/anki/template/README.anki b/anki/template/README.anki old mode 100644 new mode 100755 diff --git a/anki/template/__init__.py b/anki/template/__init__.py index 955518123d9..7d7306fadf2 100644 --- a/anki/template/__init__.py +++ b/anki/template/__init__.py @@ -2,6 +2,15 @@ from anki.template.view import View def render(template, context=None, **kwargs): + """ + Given the template and its fields, create the html of the card. + + template -- a template with mustache + context -- a dictionnary of fields, also containing the value for + Tags, Type, Deck, Subdeck, Fields, FrontSide (on the + back). Containing "cn:1" with n the ord of the field + kwargs -- more context, passed as function argument + """ context = context and context.copy() or {} context.update(kwargs) return Template(template, context).render() diff --git a/anki/template/furigana.py b/anki/template/furigana.py index 929b0817d87..741d62839f5 100644 --- a/anki/template/furigana.py +++ b/anki/template/furigana.py @@ -3,6 +3,7 @@ # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # Based off Kieran Clancy's initial implementation. +"""Some templates for Japan language""" import re from anki.hooks import addHook diff --git a/anki/template/hint.py b/anki/template/hint.py index ad4e9e6b44e..bc13d0e2cd5 100644 --- a/anki/template/hint.py +++ b/anki/template/hint.py @@ -6,6 +6,13 @@ from anki.lang import _ def hint(txt, extra, context, tag, fullname): + """Some HTML which show «show tag» (with show localized). When it is clicked, this text is replaced by txt. + + keyword arguments: + txt -- the hint + tag -- the name of the thing which is shown or not + extra, context, fullname not used. + """ if not txt.strip(): return "" # random id @@ -18,3 +25,4 @@ def hint(txt, extra, context, tag, fullname): def install(): addHook('fmod_hint', hint) +#This hook is never called in the current code diff --git a/anki/template/template.py b/anki/template/template.py index c30af2c27d0..ea83e3be361 100644 --- a/anki/template/template.py +++ b/anki/template/template.py @@ -5,6 +5,11 @@ from anki.template import hint; hint.install() clozeReg = r"(?si)\{\{(c)%s::(.*?)(::(.*?))?\}\}" +"""clozeReg % n is the regexp recognizing the occurrences of close number n. + clozeReg %.+? is the regexp recognizing any cloze occurrence. + group 0 is c. Group 1 is the cloze, group 2 is the hint with ::, group 3 is the hint. +""" +#(?si) means .dot match all strings, ignore case modifiers = {} def modifier(symbol): @@ -23,6 +28,7 @@ def set_modifier(func): def get_or_attr(obj, name, default=None): + """If its a susbscriptable: obj[name]. Else obj.name. Else default""" try: return obj[name] except KeyError: @@ -35,10 +41,24 @@ def get_or_attr(obj, name, default=None): class Template: + """TODO + + A template object contains: + template -- ; see ../models.py + context -- a dictionnary containing at least: + ** the fields + ** the value for Tags, Type, Deck, Subdeck, Fields, FrontSide (on + the back). + ** Containing "cn:1" with n the ord of the field + """ # The regular expression used to find a #section + # I.e. {{# or {{^ + # from {{#foo}}bar{{/foo}} return ({{#foo}}bar{{/foo}},foo,bar) section_re = None - # The regular expression used to find a tag. + # The regular expression used to find a tag. The tag can start by + # #, =, &, !, >,{ or the empty string. + # from {{sfoo}} return ({{sfoo}},s,foo) tag_re = None # Opening tag delimiter @@ -65,16 +85,20 @@ def render(self, template=None, context=None, encoding=None): def compile_regexps(self): """Compiles our section and tag regular expressions.""" + #Opening and closing tag. Currently {{ and }} tags = { 'otag': re.escape(self.otag), 'ctag': re.escape(self.ctag) } + # See the comment for section_re section = r"%(otag)s[\#|^]([^\}]*)%(ctag)s(.+?)%(otag)s/\1%(ctag)s" self.section_re = re.compile(section % tags, re.M|re.S) + # See the comment for tag_re tag = r"%(otag)s(#|=|&|!|>|\{)?(.+?)\1?%(ctag)s+" self.tag_re = re.compile(tag % tags) def render_sections(self, template, context): - """Expands sections.""" + """replace {{#foo}}bar{{/foo}} and {{^foo}}bar{{/foo}} by + their normal value.""" while 1: match = self.section_re.search(template) if match is None: @@ -83,7 +107,8 @@ def render_sections(self, template, context): section, section_name, inner = match.group(0, 1, 2) section_name = section_name.strip() - # check for cloze + # val will contain the content of the field considered + # right now val = None m = re.match(r"c[qa]:(\d+):(.+)", section_name) if m: @@ -96,7 +121,9 @@ def render_sections(self, template, context): val = get_or_attr(context, section_name, None) replacer = '' + # Whether it's {{^ inverted = section[2] == "^" + # Ensuring we don't consider whitespace in wval if val: val = stripHTMLMedia(val).strip() if (val and not inverted) or (not val and inverted): @@ -107,7 +134,8 @@ def render_sections(self, template, context): return template def render_tags(self, template, context): - """Renders all the tags in a template for a context.""" + """Renders all the tags in a template for a context. Normally + {{# and {{^ are already removed.""" repCount = 0 while 1: if repCount > 100: @@ -115,10 +143,12 @@ def render_tags(self, template, context): break repCount += 1 + # search for some {{foo}} match = self.tag_re.search(template) if match is None: break + # tag, tag_type, tag_name = match.group(0, 1, 2) tag_name = tag_name.strip() try: @@ -159,7 +189,7 @@ def render_unescaped(self, tag_name=None, context=None): mods, tag = parts[:-1], parts[-1] #py3k has *mods, tag = parts txt = get_or_attr(context, tag) - + #Since 'text:' and other mods can affect html on which Anki relies to #process clozes, we need to make sure clozes are always #treated after all the other mods, regardless of how they're specified diff --git a/anki/utils.py b/anki/utils.py index 6272a858011..bc52d186c91 100644 --- a/anki/utils.py +++ b/anki/utils.py @@ -135,6 +135,7 @@ def fmtFloat(float_value, point=1): reMedia = re.compile("(?i)]+src=[\"']?([^\"'>]+)[\"']?[^>]*>") def stripHTML(s): + """Removes comment, style, script, and all tags. Replace entities by their unicode value""" s = reComment.sub("", s) s = reStyle.sub("", s) s = reScript.sub("", s) @@ -143,7 +144,8 @@ def stripHTML(s): return s def stripHTMLMedia(s): - "Strip HTML but keep media filenames" + """Removes comment, style, script, and all tags. Replace images by +their url. Replace entities by their unicode value""" s = reMedia.sub(" \\1 ", s) return stripHTML(s) @@ -158,6 +160,7 @@ def minimizeHTML(s): return s def htmlToTextLine(s): + """Transform a field into a html value to show in the browser list of cards.""" s = s.replace("
", " ") s = s.replace("
", " ") s = s.replace("
", " ") @@ -169,6 +172,7 @@ def htmlToTextLine(s): return s def entsToTxt(html): + """html, where entities are replaced by their unicode character.""" # entitydefs defines nbsp as \xa0 instead of a standard space, so we # replace it first html = html.replace(" ", " ") @@ -193,6 +197,8 @@ def fixup(m): return reEnts.sub(fixup, html) def bodyClass(col, card): + """A string, containing "card", card position, and whether it's + nightMode""" bodyclass = "card card%d" % (card.ord+1) if col.conf.get("nightMode"): bodyclass += " nightMode" @@ -266,6 +272,7 @@ def joinFields(list): return "\x1f".join(list) def splitFields(string): + """Transform the fields as in the database in a list of field""" return string.split("\x1f") # Checksums @@ -324,7 +331,20 @@ def noBundledLibs(): os.environ["LD_LIBRARY_PATH"] = oldlpath def call(argv, wait=True, **kwargs): - "Execute a command. If WAIT, return exit code." + """Execute a command and return its return code. + + If wait is set to False, don't wait and return immediatly 0 + (i.e. correct exit number) + return -1 if executing the command raises OSErrors. + + If the returned value is considered as a Boolean, it returns + whether the call returned an error. + + Keyword arguments + argv -- the command to execute + wait -- whether to wait for the end of the call before returning + **kwargs -- arguments given to subprocess.Popen. + """ # ensure we don't open a separate window for forking process on windows if isWin: si = subprocess.STARTUPINFO() @@ -377,6 +397,10 @@ def invalidFilename(str, dirsep=True): return "." def platDesc(): + """{system}:{version}, where system is mac, win, or lin. + + It is theoretically resistant to system call interuption. + """ # we may get an interrupted system call, so try this in a loop n = 0 theos = "unknown" diff --git a/aqt/__init__.py b/aqt/__init__.py index 073ebe7713f..a9796c26f70 100644 --- a/aqt/__init__.py +++ b/aqt/__init__.py @@ -43,11 +43,24 @@ # ensures only one copy of the window is open at once, and provides # a way for dialogs to clean up asynchronously when collection closes +# A window object must contain: +# - windowState(): +# - activateWindow() +# - raise_() +# - Either: +# -- If it can be closed immediatly: +# --- silentlyClose must exists and be truthy, +# --- have a function close() defined +# -- Or, if some actions must be done before closing: +# --- define a method closeWithCallback(callback) +# --- This method should ensure a safe closure of the window +# --- and then call callback + +# A window class must contain: +# - A constructor, which return a window object. + # to integrate a new window: # - add it to _dialogs -# - define close behaviour, by either: -# -- setting silentlyClose=True to have it close immediately -# -- define a closeWithCallback() method # - have the window opened via aqt.dialogs.open(, self) # - have a method reopen(*args), called if the user ask to open the window a second time. Arguments passed are the same than for original opening. @@ -58,7 +71,12 @@ preferences class DialogManager: + """Associating to a window name a pair (as a list...) + The element associated to WindowName Is composed of: + First element is the class to use to create the window WindowName. + Second element is the instance of this window, if it is already open. None otherwise + """ _dialogs = { "AddCards": [addcards.AddCards, None], "Browser": [browser.Browser, None], @@ -68,7 +86,17 @@ class DialogManager: "Preferences": [preferences.Preferences, None], } + def open(self, name, *args): + """Open a window of kind name. + + Open (and show) the one already opened, if it + exists. Otherwise a new one. + + keyword arguments: + args -- values passed to the opener. + name -- the name of the window to open + """ (creator, instance) = self._dialogs[name] if instance: if instance.windowState() & Qt.WindowMinimized: @@ -84,12 +112,26 @@ def open(self, name, *args): return instance def markClosed(self, name): + """Window name is now considered as closed. It removes the element from _dialogs.""" self._dialogs[name] = [self._dialogs[name][0], None] def allClosed(self): + """ + Whether all windows (except the main window) are marked as + closed. + """ return not any(x[1] for x in self._dialogs.values()) def closeAll(self, onsuccess): + """Close all windows (except the main one). Call onsuccess when it's done. + + Return True if some window needed closing. + None otherwise + + Keyword arguments: + onsuccess -- the function to call when the last window is closed. + """ + # can we close immediately? if self.allClosed(): onsuccess() @@ -101,6 +143,7 @@ def closeAll(self, onsuccess): continue def callback(): + """Call onsuccess if all window (except main) are closed.""" if self.allClosed(): onsuccess() else: @@ -117,6 +160,7 @@ def callback(): dialogs = DialogManager() + # Language handling ########################################################################## # Qt requires its translator to be installed before any GUI widgets are @@ -178,7 +222,7 @@ def __init__(self, argv): self._argv = argv def secondInstance(self): - # we accept only one command line argument. if it's missing, send + # we accept only one command line argument. If it's missing, send # a blank screen to just raise the existing window opts, args = parseArgs(self._argv) buf = "raise" diff --git a/aqt/addcards.py b/aqt/addcards.py index c00625c458d..19c00d83fcb 100644 --- a/aqt/addcards.py +++ b/aqt/addcards.py @@ -1,6 +1,7 @@ # Copyright: Ankitects Pty Ltd and contributors # -*- coding: utf-8 -*- # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +"""The window obtained from main by pressing A, or clicking on "Add".""" from anki.lang import _ from aqt.qt import * @@ -78,6 +79,7 @@ def setupButtons(self): self.historyButton = b def setAndFocusNote(self, note): + """Add note as the content of the editor. Focus in the first element.""" self.editor.setNote(note, focusTo=0) def onModelChange(self): @@ -105,6 +107,17 @@ def onModelChange(self): self.editor.setNote(note) def onReset(self, model=None, keep=False): + """Create a new note and set it with the current field values. + + keyword arguments + model -- not used + keep -- Whether the old note was saved in the collection. In + this case, remove non sticky fields. Otherwise remove the last + temporary note (it is replaced by a new one). + """ + #Called with keep set to True from _addCards + #Called with default keep __init__, from hook "reset" + #Meaning of the word keep guessed. Not clear. oldNote = self.editor.note note = self.mw.col.newNote() flds = note.model()['flds'] @@ -179,9 +192,14 @@ def addNote(self, note): return note def addCards(self): + """Adding the content of the fields as a new note""" + #Save edits in the fields, and call _addCards self.editor.saveNow(self._addCards) def _addCards(self): + """Adding the content of the fields as a new note. + + Assume that the content of the GUI saved in the model.""" self.editor.saveAddModeVars() note = self.editor.note note = self.addNote(note) @@ -202,9 +220,15 @@ def keyPressEvent(self, evt): return QDialog.keyPressEvent(self, evt) def reject(self): + """Close the window. + + If data would be lost, ask for confirmation""" self.ifCanClose(self._reject) def _reject(self): + """Close the window. + + Don't check whether data will be lost""" remHook('reset', self.onReset) remHook('currentModelChanged', self.onModelChange) clearAudioQueue() diff --git a/aqt/addons.py b/aqt/addons.py index 7c217c6b372..aea7c2222ef 100644 --- a/aqt/addons.py +++ b/aqt/addons.py @@ -1,6 +1,15 @@ # Copyright: Ankitects Pty Ltd and contributors # -*- coding: utf-8 -*- # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +"""Module for managing add-ons. + +An add-on here is defined as a subfolder in the add-on folder containing a file __init__.py +A managed add-on is an add-on whose folder's name contains only +digits. + +dir -- the name of the subdirectory of the add-on in the add-on manager +""" + import io import json import re @@ -24,6 +33,10 @@ from anki.sync import AnkiRequestsClient class AddonManager: + """ + dirty -- whether an add-on is loaded + mw -- the main window + """ ext = ".ankiaddon" _manifest_schema = { @@ -49,6 +62,14 @@ def __init__(self, mw): sys.path.insert(0, self.addonsFolder()) def allAddons(self): + """List of installed add-ons' folder name + + In alphabetical order of folder name. I.e. add-on number for downloaded add-ons. + Reverse order if the environment variable ANKIREVADDONS is set. + + A folder is an add-on folder iff it contains __init__.py. + + """ l = [] for d in os.listdir(self.addonsFolder()): path = self.addonsFolder(d) @@ -61,10 +82,24 @@ def allAddons(self): return l def managedAddons(self): + """List of managed add-ons. + + In alphabetical order of folder name. I.e. add-on number for downloaded add-ons. + Reverse order if the environment variable ANKIREVADDONS is set. + """ return [d for d in self.allAddons() if re.match(r"^\d+$", d)] def addonsFolder(self, dir=None): + """Path to a folder. + + To the add-on folder by default, guaranteed to exists. + If dir is set, then the path to the add-on dir, not guaranteed + to exists + + dir -- TODO + """ + root = self.mw.pm.addonFolder() if not dir: return root @@ -94,6 +129,7 @@ def onAddonsDialog(self): ###################################################################### def _addonMetaPath(self, dir): + """Path of the configuration of the addon dir""" return os.path.join(self.addonsFolder(dir), "meta.json") def addonMeta(self, dir): @@ -129,6 +165,11 @@ def toggleEnabled(self, dir, enable=None): self.writeAddonMeta(dir, meta) def addonName(self, dir): + """The name of the addon. + + It is found either in "name" parameter of the configuration in + directory dir of the add-on directory. + Otherwise dir is used.""" return self.addonMeta(dir).get("name", dir) def annotatedName(self, dir): @@ -164,7 +205,7 @@ def _disableConflicting(self, dir, conflicts=None): for package in found: self.toggleEnabled(package, enable=False) - + return found # Installing and deleting add-ons @@ -191,7 +232,7 @@ def install(self, file, manifest=None): zfile = ZipFile(file) except zipfile.BadZipfile: return False, "zip" - + with zfile: file_manifest = self.readManifestFile(zfile) if manifest: @@ -205,7 +246,6 @@ def install(self, file, manifest=None): conflicts) meta = self.addonMeta(package) self._install(package, zfile) - schema = self._manifest_schema["properties"] manifest_meta = {k: v for k, v in manifest.items() if k in schema and schema[k]["meta"]} @@ -219,7 +259,7 @@ def _install(self, dir, zfile): base = self.addonsFolder(dir) if os.path.exists(base): self.backupUserFiles(dir) - if not self.deleteAddon(dir): + if not self.deleteAddon(dir): # To install, previous version should be deleted. If it can't be deleted for an unkwown reason, we try to put everything back in previous state. self.restoreUserFiles(dir) return @@ -238,8 +278,8 @@ def _install(self, dir, zfile): continue zfile.extract(n, base) - # true on success def deleteAddon(self, dir): + """Delete the add-on folder of add-on dir. Returns True on success""" try: send2trash(self.addonsFolder(dir)) return True @@ -250,7 +290,7 @@ def deleteAddon(self, dir): # Processing local add-on files ###################################################################### - + def processPackages(self, paths): log = [] errs = [] @@ -315,6 +355,7 @@ def downloadIds(self, ids): ###################################################################### def checkForUpdates(self): + """The list of add-ons not up to date. Compared to the server's information.""" client = AnkiRequestsClient() # get mod times @@ -337,6 +378,10 @@ def checkForUpdates(self): self.mw.progress.finish() def _getModTimes(self, client, chunk): + """The list of (id,mod time) for add-ons whose id is in chunk. + + client -- an ankiRequestsclient + chunck -- a list of add-on number""" resp = client.get( aqt.appShared + "updates/" + ",".join(chunk)) if resp.status_code == 200: @@ -345,6 +390,8 @@ def _getModTimes(self, client, chunk): raise Exception("Unexpected response code from AnkiWeb: {}".format(resp.status_code)) def _updatedIds(self, mods): + """Given a list of (id,last mod on server), returns the sublist of + add-ons not up to date.""" updated = [] for dir, ts in mods: sid = str(dir) @@ -355,10 +402,19 @@ def _updatedIds(self, mods): # Add-on Config ###################################################################### + """Dictionnary from modules to function to apply when add-on + manager is called on this config.""" _configButtonActions = {} + """Dictionnary from modules to function to apply when add-on + manager ends an update. Those functions takes the configuration, + parsed as json, in argument.""" _configUpdatedActions = {} def addonConfigDefaults(self, dir): + """The (default) configuration of the addon whose + name/directory is dir. + + This file should be called config.json""" path = os.path.join(self.addonsFolder(dir), "config.json") try: with open(path, encoding="utf8") as f: @@ -367,6 +423,7 @@ def addonConfigDefaults(self, dir): return None def addonConfigHelp(self, dir): + """The configuration of this addon, obtained as configuration""" path = os.path.join(self.addonsFolder(dir), "config.md") if os.path.exists(path): with open(path, encoding="utf-8") as f: @@ -375,18 +432,38 @@ def addonConfigHelp(self, dir): return "" def addonFromModule(self, module): + """Returns the string of module before the first dot""" return module.split(".")[0] def configAction(self, addon): + """The function to call for addon when add-on manager ask for + edition of its configuration.""" return self._configButtonActions.get(addon) def configUpdatedAction(self, addon): + """The function to call for addon when add-on edition has been done + using add-on manager. + + """ return self._configUpdatedActions.get(addon) # Add-on Config API ###################################################################### def getConfig(self, module): + """The current configuration. + + More precisely: + -None if the module has no file config.json + -otherwise the union of: + --default config from config.json + --the last version of the config, as saved in meta + + Note that if you edited the dictionary obtained from the + configuration file without calling self.writeConfig(module, + config), then getConfig will not return current config + + """ addon = self.addonFromModule(module) # get default config config = self.addonConfigDefaults(addon) @@ -399,14 +476,35 @@ def getConfig(self, module): return config def setConfigAction(self, module, fn): + """Change the action of add-on manager for the edition of the + current add-ons config. + + Each time the user click in the add-on manager on the button + "config" button, fn is called. Unless fn is falsy, in which + case the standard procedure is used + + Keyword arguments: + module -- the module/addon considered + fn -- a function taking no argument, or a falsy value + """ addon = self.addonFromModule(module) self._configButtonActions[addon] = fn def setConfigUpdatedAction(self, module, fn): + """Allow a function to add on new configurations. + + Each time the configuration of module is modified in the + add-on manager, fn is called on the new configuration. + + Keyword arguments: + module -- __name__ from module's code + fn -- A function taking the configuration, parsed as json, in + """ addon = self.addonFromModule(module) self._configUpdatedActions[addon] = fn def writeConfig(self, module, conf): + """The config for the module whose name is module is now conf""" addon = self.addonFromModule(module) meta = self.addonMeta(addon) meta['config'] = conf @@ -416,24 +514,29 @@ def writeConfig(self, module, conf): ###################################################################### def _userFilesPath(self, sid): + """The path of the user file's folder.""" return os.path.join(self.addonsFolder(sid), "user_files") def _userFilesBackupPath(self): + """A path to use for back-up. It's independent of the add-on number.""" return os.path.join(self.addonsFolder(), "files_backup") def backupUserFiles(self, sid): + """Move user file's folder to a folder called files_backup in the add-on folder""" p = self._userFilesPath(sid) if os.path.exists(p): os.rename(p, self._userFilesBackupPath()) def restoreUserFiles(self, sid): + """Move the back up of user file's folder to its normal location in + the folder of the addon sid""" p = self._userFilesPath(sid) bp = self._userFilesBackupPath() # did we back up userFiles? if not os.path.exists(bp): return os.rename(bp, p) - + # Web Exports ###################################################################### @@ -442,7 +545,7 @@ def restoreUserFiles(self, sid): def setWebExports(self, module, pattern): addon = self.addonFromModule(module) self._webExports[addon] = pattern - + def getWebExports(self, addon): return self._webExports.get(addon) @@ -500,7 +603,7 @@ def reject(self): def redrawAddons(self): addonList = self.form.addonList mgr = self.mgr - + self.addons = [(mgr.annotatedName(d), d) for d in mgr.allAddons()] self.addons.sort() @@ -588,7 +691,7 @@ def onInstallFiles(self, paths=None): key="addons", multi=True) if not paths: return False - + log, errs = self.mgr.processPackages(paths) if log: @@ -630,6 +733,12 @@ def onCheckForUpdates(self): self.redrawAddons() def onConfig(self): + """Assuming a single addon is selected, either: + -if this add-on as a special config, set using setConfigAction, with a + truthy value, call this config. + -otherwise, call the config editor on the current config of + this add-on""" + addon = self.onlyOneSelected() if not addon: return @@ -744,6 +853,16 @@ def reject(self): super().reject() def accept(self): + """ + Transform the new config into json, and either: + -pass it to the special config function, set using + setConfigUpdatedAction if it exists, + -or save it as configuration otherwise. + + If the config is not proper json, show an error message and do + nothing. + -if the special config is falsy, just save the value + """ txt = self.form.editor.toPlainText() try: new_conf = json.loads(txt) @@ -761,6 +880,6 @@ def accept(self): act = self.mgr.configUpdatedAction(self.addon) if act: act(new_conf) - + self.onClose() super().accept() diff --git a/aqt/browser.py b/aqt/browser.py index c0d71699623..f0e74fef0a0 100644 --- a/aqt/browser.py +++ b/aqt/browser.py @@ -33,6 +33,12 @@ class DataModel(QAbstractTableModel): + """ + sortKey -- never used + activeCols -- the list of columns to display in the browser + cards -- the set of cards corresponding to current browser's search + cardObjs -- dictionnady from card's id to the card object. + """ def __init__(self, browser): QAbstractTableModel.__init__(self) self.browser = browser @@ -384,6 +390,10 @@ def paint(self, painter, option, index): # fixme: respond to reset+edit hooks class Browser(QMainWindow): + """model: the data model (and not a card model !) + + card: the card in the reviewer when the browser was opened, or the last selected card. + """ def __init__(self, mw): QMainWindow.__init__(self, None, Qt.Window) @@ -623,6 +633,7 @@ def updateTitle(self): return selected def onReset(self): + """Remove the note from the browser. Redo the search""" self.editor.setNote(None) self.search() @@ -1211,6 +1222,7 @@ def _revlogData(self, cs): ###################################################################### def selectedCards(self): + """The list of selected card's id""" return [self.model.cards[idx.row()] for idx in self.form.tableView.selectionModel().selectedRows()] @@ -1245,9 +1257,20 @@ def onHelp(self): ###################################################################### def onChangeModel(self): + """Starts a GUI letting the user change the model of notes. + + If multiple note type are selected, then show a warning + instead. It saves the editor content before doing any other + change it. + + """ self.editor.saveNow(self._onChangeModel) def _onChangeModel(self): + """Starts a GUI letting the user change the model of notes. + + If multiple note type are selected, then show a warning instead. + Don't call this directly, call onChangeModel. """ nids = self.oneModelNotes() if nids: ChangeModel(self, nids) @@ -1932,7 +1955,13 @@ def focusCid(self, cid): class ChangeModel(QDialog): + """The dialog window, obtained in the browser by selecting cards and + Cards>Change Note Type. It allows to change the type of a note + from one type to another. + + """ def __init__(self, browser, nids): + """Create and open a dialog for changing model""" QDialog.__init__(self, browser) self.browser = browser self.nids = nids @@ -1974,14 +2003,24 @@ def setup(self): self.pauseUpdate = False def onReset(self): + """Change the model changer GUI to the current note type.""" self.modelChanged(self.browser.col.models.current()) def modelChanged(self, model): + """Change the model changer GUI to model + + This should be used if the destination model has been changed. + """ self.targetModel = model self.rebuildTemplateMap() self.rebuildFieldMap() def rebuildTemplateMap(self, key=None, attr=None): + """Change the "Cards" subwindow of the Change Note Type. + + Actually, if key and attr are given, it may change another + subwindow, so the same code is reused for fields. + """ if not key: key = "t" attr = "tmpls" @@ -2017,6 +2056,7 @@ def rebuildTemplateMap(self, key=None, attr=None): setattr(self, key + "indices", indices) def rebuildFieldMap(self): + """Change the "Fields" subwindow of the Change Note Type.""" return self.rebuildTemplateMap(key="f", attr="flds") def onComboChanged(self, i, cb, key): @@ -2040,6 +2080,18 @@ def onComboChanged(self, i, cb, key): indices[cb] = i def getTemplateMap(self, old=None, combos=None, new=None): + """A map from template's ord of the old model to template's ord of the new + model. Or None if no template + + Contrary to what this name indicates, the method may be used + without templates. In getFieldMap it is used for fields + + keywords parameter: + old -- the list of templates of the old model + combos -- the python list of gui's list of template + new -- the list of templates of the new model + If old is not given, the other two arguments are not used. + """ if not old: old = self.oldModel['tmpls'] combos = self.tcombos @@ -2048,7 +2100,7 @@ def getTemplateMap(self, old=None, combos=None, new=None): for i, f in enumerate(old): idx = combos[i].currentIndex() if idx == len(new): - # ignore + # ignore. len(new) corresponds probably to nothing in the list map[f['ord']] = None else: f2 = new[idx] @@ -2056,25 +2108,37 @@ def getTemplateMap(self, old=None, combos=None, new=None): return map def getFieldMap(self): + """Associating to each field's ord of the source model a field's + ord (or None) of the new model.""" return self.getTemplateMap( old=self.oldModel['flds'], combos=self.fcombos, new=self.targetModel['flds']) def cleanup(self): + """Actions to end this gui. + + Remove hook related to this window, and potentially its model chooser. + Save the geometry of the current window in order to keep it for a new reordering + """ remHook("reset", self.onReset) remHook("currentModelChanged", self.onReset) self.modelChooser.cleanup() saveGeom(self, "changeModel") def reject(self): + """Cancelling the changes.""" self.cleanup() return QDialog.reject(self) def accept(self): + """Procede to changing the model, according to the content of the GUI. + + TODO""" # check maps fmap = self.getFieldMap() cmap = self.getTemplateMap() + #If there are cards which are sent to nothing: if any(True for c in list(cmap.values()) if c is None): if not askUser(_("""\ Any cards mapped to nothing will be deleted. \ diff --git a/aqt/clayout.py b/aqt/clayout.py index ea198d76345..84cd5da14b4 100644 --- a/aqt/clayout.py +++ b/aqt/clayout.py @@ -2,6 +2,9 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +"""The window used to: +* edit a note type +* preview the different cards of a note.""" import collections import re @@ -19,6 +22,21 @@ from anki.lang import _, ngettext class CardLayout(QDialog): + """TODO + + An object of class CardLayout contains: + nw -- the main window + parent -- the parent of the caller, by default the main window + note -- the note object considered + ord -- the order of the card considered + col -- the current collection + mm -- The model manager + model -- the model of the note + addMode -- if the card layout is called for a new card (in this case, it is temporary added to the db). True if its called from models.py, false if its called from edit.py + emptyFields -- the list of fields which are empty. Used only if addMode is true + redrawing -- is it currently redrawing (forbid savecard and onCardSeleceted) + cards -- the list of cards of the current note, each with their template. + """ def __init__(self, mw, note, ord=0, parent=None, addMode=False): QDialog.__init__(self, parent or mw, Qt.Window) @@ -61,10 +79,14 @@ def __init__(self, mw, note, ord=0, parent=None, addMode=False): self.setFocus() def redraw(self): + """TODO + update the list of card + """ did = None if hasattr(self.parent,"deckChooser"): did = self.parent.deckChooser.selectedId() self.cards = self.col.previewCards(self.note, 2, did=did) + #the list of cards of this note, with all templates idx = self.ord if idx >= len(self.cards): self.ord = len(self.cards) - 1 @@ -80,6 +102,7 @@ def setupShortcuts(self): QShortcut(QKeySequence("Ctrl+%d" % i), self, activated=lambda i=i: self.selectCard(i)) def selectCard(self, n): + """Change ord to n-1 and redraw.""" self.ord = n-1 self.redraw() @@ -93,6 +116,7 @@ def setupTopArea(self): def updateTopArea(self): cnt = self.mw.col.models.useCount(self.model) + #number of notes using this model self.topAreaForm.changesLabel.setText(ngettext( "Changes below will affect the %(cnt)d note that uses this card type.", "Changes below will affect the %(cnt)d notes that use this card type.", @@ -100,6 +124,8 @@ def updateTopArea(self): self.updateCardNames() def updateCardNames(self): + """ In the list of card name, change them according to + current's name""" self.redrawing = True combo = self.topAreaForm.templatesBox combo.clear() @@ -109,12 +135,19 @@ def updateCardNames(self): self.redrawing = False def _summarizedName(self, tmpl): + """Compute the text appearing in the list of templates, on top of the window + + tmpl -- a template object + """ return "{}: {} -> {}".format( tmpl['name'], self._fieldsOnTemplate(tmpl['qfmt']), self._fieldsOnTemplate(tmpl['afmt'])) def _fieldsOnTemplate(self, fmt): + """List of tags found in fmt, separated by +, limited to 30 characters + (not counting the +), in lexicographic order, with +... if some are + missings.""" matches = re.findall("{{[^#/}]+?}}", fmt) charsAllowed = 30 result = collections.OrderedDict() @@ -203,6 +236,7 @@ def updateMainArea(self): g.setTitle(g.title() + _(" (1 of %d)") % max(cnt, 1)) def onRemove(self): + """ Remove the current template, except if it would leave a note without card. Ask user for confirmation""" if len(self.model['tmpls']) < 2: return showInfo(_("At least one card type is required.")) idx = self.ord @@ -296,6 +330,10 @@ def cancelPreviewTimer(self): self._previewTimer = None def _renderPreview(self): + """ + change the answer and question side of the preview + windows. Change the list of name of cards. + """ self.cancelPreviewTimer() c = self.card @@ -303,6 +341,8 @@ def _renderPreview(self): bodyclass = bodyClass(self.mw.col, c) + # deal with [[type:, image and remove sound of the card's + # question and answer q = ti(mungeQA(self.mw.col, c.q(reload=True))) q = runFilter("prepareQA", q, c, "clayoutQuestion") @@ -314,7 +354,8 @@ def _renderPreview(self): self.pform.backWeb.eval("_showAnswer(%s, '%s');" % (json.dumps(a), bodyclass)) clearAudioQueue() - if c.id not in self.playedAudio: + if c.id not in self.playedAudio: # this ensure that audio is + # played only once until a card is selected again playFromText(c.q()) playFromText(c.a()) self.playedAudio[c.id] = True @@ -322,6 +363,16 @@ def _renderPreview(self): self.updateCardNames() def maybeTextInput(self, txt, type='q'): + """HTML: A default example for [[type:, which is shown in the preview + window. + + On the question side, it shows "exomple", on the answer side + it shows the correction, for when the right answer is "an + example". + + txt -- the card type + type -- a side. 'q' for question, 'a' for answer + """ if "[[type:" not in txt: return txt origLen = len(txt) @@ -354,6 +405,7 @@ def onRename(self): self.redraw() def onReorder(self): + """Asks user for a new position for current template. Move to this position if it is a valid position.""" n = len(self.cards) cur = self.card.template()['ord']+1 pos = getOnlyText( @@ -384,6 +436,7 @@ def _newCardName(self): return name def onAddCard(self): + """Ask for confirmation and create a copy of current card as the last template""" cnt = self.mw.col.models.useCount(self.model) txt = ngettext("This will create %d card. Proceed?", "This will create %d cards. Proceed?", cnt) % cnt @@ -538,9 +591,11 @@ def _addField(self, widg, field, font, size): ###################################################################### def accept(self): + """Same as reject.""" self.reject() def reject(self): + """ Close the window and save the current version of the model""" self.cancelPreviewTimer() clearAudioQueue() if self.addMode: diff --git a/aqt/deckbrowser.py b/aqt/deckbrowser.py index 8c56d971755..b1eef581fd6 100644 --- a/aqt/deckbrowser.py +++ b/aqt/deckbrowser.py @@ -2,6 +2,14 @@ # -*- coding: utf-8 -*- # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +# A node is composed of +# (name of the deck, +# its id, +# its number of due cards, +# number of reviews of cards in learning which will occur today, +# its number of new cards to see today, +# its list of children) from aqt.qt import * from aqt.utils import askUser, getOnlyText, openLink, showWarning, shortcut, \ openHelp @@ -84,6 +92,7 @@ def _selDeck(self, did): """ def _renderPage(self, reuse=False): + """Write the HTML of the deck browser. Move to the last vertical position.""" if not reuse: self._dueTree = self.mw.col.sched.deckDueTree() self.__renderPage(None) @@ -129,9 +138,15 @@ def _countWarn(self): "
"))) def _renderDeckTree(self, nodes, depth=0): + """Html used to show the deck tree. + + keyword arguments + depth -- the number of ancestors, excluding itself + nodes -- A list of nodes, to render, with the same parent. See top of this file for detail""" if not nodes: return "" if depth == 0: + #Toplevel buf = """ %s%s %s""" % ( @@ -147,6 +162,14 @@ def _renderDeckTree(self, nodes, depth=0): return buf def _deckRow(self, node, depth, cnt, nameMap): + """The HTML for a single deck (and its descendant) + + Keyword arguments: + node -- see in the introduction of the file for a node description + depth -- indentation argument (number of ancestors) + cnt -- the number of sibling, counting itself + nameMap -- dictionnary, associating to a deck id its node + """ name, did, due, lrn, new, children = node deck = self.mw.col.decks.get(did) if did == 1 and cnt > 1 and not children: diff --git a/aqt/downloader.py b/aqt/downloader.py index 37127bc1079..05c9109c4df 100644 --- a/aqt/downloader.py +++ b/aqt/downloader.py @@ -2,7 +2,9 @@ # -*- coding: utf-8 -*- # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -import time, re +"""Everything required to download an add-on, when we already have the number.""" + +import time, re, traceback from aqt.qt import * from anki.sync import AnkiRequestsClient from anki.hooks import addHook, remHook @@ -10,7 +12,10 @@ from anki.lang import _ def download(mw, code): - "Download addon from AnkiWeb. Caller must start & stop progress diag." + """add-on file and add-on name whose number is code. Downloaded + from ankiweb. Or a pair with "error" and the error code. + + Caller must start & stop progress diag.""" # create downloading thread thread = Downloader(code) done = False @@ -34,10 +39,13 @@ def onRecv(): return "error", thread.error class Downloader(QThread): + """Class used to download add-on. Initialized with add-on number. + Once .run is executed, .data contains the data and .fname the name""" recv = pyqtSignal() def __init__(self, code): + """code: the add-on number""" QThread.__init__(self) self.code = code self.error = None diff --git a/aqt/editcurrent.py b/aqt/editcurrent.py index f5da5c4e1ab..9b73f184f2e 100644 --- a/aqt/editcurrent.py +++ b/aqt/editcurrent.py @@ -35,6 +35,10 @@ def __init__(self, mw): self.mw.progress.timer(100, lambda: self.editor.web.setFocus(), False) def onReset(self): + """ + Reload the note, discarding change. + If the note is deleted, close the window. + """ # lazy approach for now: throw away edits try: n = self.editor.note @@ -52,7 +56,7 @@ def onReset(self): def reopen(self,mw): tooltip("Please finish editing the existing card first.") self.onReset() - + def reject(self): self.saveAndClose() diff --git a/aqt/editor.py b/aqt/editor.py index ca9abc9860e..1a0c15421ca 100644 --- a/aqt/editor.py +++ b/aqt/editor.py @@ -1,6 +1,12 @@ # -*- coding: utf-8 -*- # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +""" +The part of the window used to edit notes. It is used for adding +note, editing existing note, and as the lower part of the browser. + +""" + import re import urllib.request, urllib.parse, urllib.error import warnings @@ -40,7 +46,16 @@ # caller is responsible for resetting note on reset class Editor: + """ + _links -- associate to each javascript command an action to do. ATTENTION: it is directly a function, and not a method from this class. Thus if you override the method, the function is not automatically modified. + addMode -- Whether editor is called from addcard.py + currentField -- The index of the field currently selected. Or None if no fiel is selected. + card -- the card selected in the browser/in the edit window + """ def __init__(self, mw, widget, parentWindow, addMode=False): + """TODO + + addMode -- Whether editor is called from addcard.py""" self.mw = mw self.widget = widget self.parentWindow = parentWindow @@ -71,6 +86,7 @@ def setupWeb(self): self.web.onBridgeCmd = self.onBridgeCmd self.outerLayout.addWidget(self.web, 1) + # List of buttons on top right of editor righttopbtns = list() righttopbtns.append(self._addButton('text_bold', 'bold', _("Bold text (Ctrl+B)"), id='bold')) righttopbtns.append(self._addButton('text_italic', 'italic', _("Italic text (Ctrl+I)"), id='italic')) @@ -94,23 +110,29 @@ def setupWeb(self): righttopbtns.append(self._addButton('media-record', 'record', _("Record audio (F5)"))) righttopbtns.append(self._addButton('more', 'more')) righttopbtns = runFilter("setupEditorButtons", righttopbtns, self) - topbuts = """ -
+ + # Fields... and Cards... button on top lefts, and + lefttopbtns = """ + """%dict(flds=_("Fields"), cards=_("Cards"), + fldsTitle=_("Customize Fields"), + cardsTitle=shortcut(_("Customize Card Templates (Ctrl+L)"))) + topbuts= """ +
+ %(lefttopbtns)s
%(rightbts)s
- """ % dict(flds=_("Fields"), cards=_("Cards"), rightbts="".join(righttopbtns), - fldsTitle=_("Customize Fields"), - cardsTitle=shortcut(_("Customize Card Templates (Ctrl+L)"))) + """ % dict(lefttopbtns = lefttopbtns, rightbts="".join(righttopbtns)) bgcol = self.mw.app.palette().window().color().name() # then load page - self.web.stdHtml(_html % ( + html = _html % ( bgcol, bgcol, topbuts, - _("Show Duplicates")), + _("Show Duplicates")) + self.web.stdHtml(html, css=["editor.css"], js=["jquery.js", "editor.js"]) @@ -128,7 +150,7 @@ def resourceToData(self, path): return 'data:%s;base64,%s' % (mime, data64.decode('ascii')) - def addButton(self, icon, cmd, func, tip="", label="", + def addButton(self, icon, cmd, func, tip="", label="", id=None, toggleable=False, keys=None, disables=True): """Assign func to bridge cmd, register shortcut, return button""" if cmd not in self._links: @@ -142,6 +164,17 @@ def addButton(self, icon, cmd, func, tip="", label="", def _addButton(self, icon, cmd, tip="", label="", id=None, toggleable=False, disables=True): + """Create a button, with the image icon, or the text of the label, or + of the cmd. It send the python command cmd. + + icon -- url to the icon. Potentially falsy + cmd -- python command to call when the button is pressed + tip -- text to show when the mouse is on the button + id -- an identifier for the html button + toggleable -- whether pressing the button should call the js function toggleEditorButton + disables -- if true, add class "perm" to the btuton + + """ if icon: if icon.startswith("qrc:/"): iconstr = icon @@ -298,7 +331,18 @@ def mungeHTML(self, txt): ###################################################################### def setNote(self, note, hide=True, focusTo=None): - "Make NOTE the current note." + """Make `note` the current note. + It's used when a new note should be set in place, or when the model change. + + if note is Falsy: + Remove tags's line. + + + keyword arguments: + note -- the new note in the editor + hide -- whether to hide the current widget + focusTo -- in which field should the focus appear + """ self.note = note self.currentField = None if self.note: @@ -312,10 +356,13 @@ def loadNoteKeepingFocus(self): self.loadNote(self.currentField) def loadNote(self, focusTo=None): + """Todo + + focusTo -- Whether focus should be set to some field.""" if not self.note: return - data = [] + data = []# field name, field content modified so that it's image's url can be used locally. for fld, val in list(self.note.items()): data.append((fld, self.mw.col.media.escapeImages(val))) self.widget.show() @@ -448,6 +495,7 @@ def saveTags(self): runHook("tagsUpdated", self.note) def saveAddModeVars(self): + """During creation of new notes, save tags to the note's model""" if self.addMode: # save tags to model m = self.note.model() @@ -455,6 +503,7 @@ def saveAddModeVars(self): self.mw.col.models.save(m) def hideCompleters(self): + "Remove tags's line" self.tags.hideCompleter() def onFocusTags(self): diff --git a/aqt/exporting.py b/aqt/exporting.py index fd9612f2bb6..8199a294e1d 100644 --- a/aqt/exporting.py +++ b/aqt/exporting.py @@ -1,6 +1,18 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +""" +mw -- the main window +col -- the collection +frm -- the formula GUIn +exporters -- A list of pairs (description of an exporter class, the class) +exporter -- An instance of the class choosen in the GUI +decks -- The list of decks option used in the GUI. All Decks and decks' name +isApkg -- Whether exporter's suffix is apkg +isVerbatim -- Whether exporter has an attribute "verbatim" set to True. Occurs only in Collection package exporter. +isTextNote -- Whether exporter has an attribute "includeTags" set to True. Occurs only in textNoteExporter. +""" + import os import re @@ -26,6 +38,11 @@ def __init__(self, mw, did=None): self.exec_() def setup(self, did): + """ + + keyword arguments: + did -- if None, then export whole anki. If did, export this deck (at least as default). + """ self.exporters = exporters() # if a deck specified, start with .apkg type selected idx = 0 diff --git a/aqt/fields.py b/aqt/fields.py index 55362d9fabb..bb909cd731e 100644 --- a/aqt/fields.py +++ b/aqt/fields.py @@ -33,6 +33,7 @@ def __init__(self, mw, note, ord=0, parent=None): ########################################################################## def fillFields(self): + """Write "ord:name" in each line""" self.currentIdx = None self.form.fieldList.clear() for c, f in enumerate(self.model['flds']): @@ -55,6 +56,9 @@ def onRowChange(self, idx): self.loadField(idx) def _uniqueName(self, prompt, ignoreOrd=None, old=""): + """Ask for a new name using prompt, and default value old. Return it. + + Unles this name is already used elsewhere, in this case, return None and show a warning. """ txt = getOnlyText(prompt, default=old) if not txt: return @@ -67,6 +71,10 @@ def _uniqueName(self, prompt, ignoreOrd=None, old=""): return txt def onRename(self): + """Ask for a new name. If required, save in in the model, and reload the content. + + Templates are edited to use the new name. requirements are also recomputed. + """ idx = self.currentIdx f = self.model['flds'][idx] name = self._uniqueName(_("New name:"), self.currentIdx, f['name']) @@ -139,6 +147,7 @@ def loadField(self, idx): f.rtl.setChecked(fld['rtl']) def saveField(self): + """Save all options in current field""" # not initialized yet? if self.currentIdx is None: return @@ -151,6 +160,7 @@ def saveField(self): fld['rtl'] = f.rtl.isChecked() def reject(self): + """Close the window. If there were some change, recompute with updateFieldCache(todo)""" self.saveField() if self.oldSortField != self.model['sortf']: self.mw.progress.start() diff --git a/aqt/main.py b/aqt/main.py index ab1eb23d0fb..4525ba78078 100644 --- a/aqt/main.py +++ b/aqt/main.py @@ -31,7 +31,26 @@ from aqt.qt import sip from anki.lang import _, ngettext +""" +self.stateShortcuts -- the list of QShortcut elements due to the actual state of the main window (i.e. reviewer or overwiew). +""" + class AnkiQt(QMainWindow): + """ + col -- The collection + state -- It's states which kind of content main shows. Either: + -- startup + -- resetRequired: during review, when edit or browser is opened, the window show "waiting for editing to finish. Resume now + -- sync + -- overview + -- review + -- profileManager + -- deckBrowser + stateShortcuts -- shortcuts related to the kind of window currently in main. + bottomWeb -- a ankiwebview, with the bottom of the main window. Shown unless for reset required. + app -- an object of class AnkiApp. + """ + def __init__(self, app, profileManager, opts, args): QMainWindow.__init__(self) self.state = "startup" @@ -54,7 +73,7 @@ def __init__(self, app, profileManager, opts, args): # were we given a file to import? if args and args[0]: self.onAppMsg(args[0]) - # Load profile in a timer so we can let the window finish init and not + # Load profile in a timer so we can let the window finish init and not # close on profile load error. if isWin: fn = self.setupProfileAfterWebviewsLoaded @@ -339,13 +358,15 @@ def loadCollection(self): try: return self._loadCollection() except Exception as e: - showWarning(_("""\ + t=_("""\ Anki was unable to open your collection file. If problems persist after \ restarting your computer, please use the Open Backup button in the profile \ manager. Debug info: -""")+traceback.format_exc()) +""")+traceback.format_exc() + showWarning(t) + print(t, file = sys.stderr) # clean up open collection if possible if self.col: try: @@ -473,6 +494,12 @@ def maybeOptimize(self): ########################################################################## def moveToState(self, state, *args): + """Call self._oldStateCleanup(state) if it exists for oldState. It seems it's the + case only for review. + remove shortcut related to this state + run hooks beforeStateChange and afterStateChange. By default they are empty. + show the bottom, unless its reset required. + """ #print("-> move from", self.state, "to", state) oldState = self.state or "dummy" cleanup = getattr(self, "_"+oldState+"Cleanup", None) @@ -515,6 +542,7 @@ def _reviewState(self, oldState): self.reviewer.show() def _reviewCleanup(self, newState): + """Run hook "reviewCleanup". Unless new state is resetRequired or review.""" if newState != "resetRequired" and newState != "review": self.reviewer.cleanup() @@ -526,7 +554,23 @@ def noteChanged(self, nid): ########################################################################## def reset(self, guiOnly=False): - "Called for non-trivial edits. Rebuilds queue and updates UI." + """Called for non-trivial edits. Rebuilds queue and updates UI. + + set Edit>undo + change state (show the bottom bar, remove shortcut from last state) + run hooks beforeStateChange and afterStateChange. By default they are empty. + call cleanup of last state. + call the hook "reset". It contains at least the onReset method + from the current window if it is browser, (and its + changeModel), editCurrent, addCard, studyDeck, + modelChooser. Reset reinitialize those window without closing + them. + + unless guiOnly: + Deal with the fact that it's potentially a new day. + Reset number of learning, review, new cards according to current decks + empty queues. Set haveQueues to true. + """ if self.col: if not guiOnly: self.col.reset() @@ -729,14 +773,14 @@ def setupStyle(self): QMenuBar { border-bottom: 1px solid #aaa; background: white; -} +} """ # qt bug? setting the above changes the browser sidebar # to white as well, so set it back buf += """ QTreeWidget { background: #eee; -} +} """ # allow addons to modify the styling @@ -767,6 +811,11 @@ def setupKeys(self): self.stateShortcuts = [] def applyShortcuts(self, shortcuts): + """A list of shortcuts. + + Keyword arguments: + shortcuts -- a list of pair (shortcut key, function called by the shortcut) + """ qshortcuts = [] for key, fn in shortcuts: scut = QShortcut(QKeySequence(key), self, activated=fn) @@ -775,10 +824,15 @@ def applyShortcuts(self, shortcuts): return qshortcuts def setStateShortcuts(self, shortcuts): + """set stateShortcuts to QShortcut from shortcuts + + run hook CURRENTSTATEStateShorcuts + """ runHook(self.state+"StateShortcuts", shortcuts) self.stateShortcuts = self.applyShortcuts(shortcuts) def clearStateShortcuts(self): + """Delete the shortcut of current state, empty stateShortcuts""" for qs in self.stateShortcuts: sip.delete(qs) self.stateShortcuts = [] @@ -825,7 +879,8 @@ def onUndo(self): self.maybeEnableUndo() def maybeEnableUndo(self): - if self.col and self.col.undoName(): + """Enable undo in the GUI if something can be undone. Call the hook undoState(somethingCanBeUndone).""" + if self.col and self.col.undoName():#Whether something can be undone self.form.actionUndo.setText(_("Undo %s") % self.col.undoName()) self.form.actionUndo.setEnabled(True) @@ -849,15 +904,23 @@ def autosave(self): ########################################################################## def onAddCard(self): + """Open the addCards window.""" aqt.dialogs.open("AddCards", self) def onBrowse(self): + """Open the browser window.""" aqt.dialogs.open("Browser", self) def onEditCurrent(self): + """Open the editing window.""" aqt.dialogs.open("EditCurrent", self) def onDeckConf(self, deck=None): + """Open the deck editor. + + According to whether the deck is dynamic or not, open distinct window + keyword arguments: + deck -- The deck to edit. If not give, current Deck""" if not deck: deck = self.col.decks.current() if deck['dyn']: @@ -872,12 +935,16 @@ def onOverview(self): self.moveToState("overview") def onStats(self): + """Open stats for selected decks + + If there are no selected deck, don't do anything.""" deck = self._selectedDeck() if not deck: return aqt.dialogs.open("DeckStats", self) def onPrefs(self): + """Open preference window""" aqt.dialogs.open("Preferences", self) def onNoteTypes(self): @@ -885,12 +952,15 @@ def onNoteTypes(self): aqt.models.Models(self, self, fromMain=True) def onAbout(self): + """Open the about window""" aqt.dialogs.open("About", self) def onDonate(self): + """Ask the OS to open the donate web page""" openLink(aqt.appDonate) def onDocumentation(self): + """Ask the OS to open the documentation web page""" openHelp("") # Importing & exporting @@ -908,6 +978,7 @@ def onImport(self): aqt.importing.onImport(self) def onExport(self, did=None): + """Open exporting window, with did as in its argument.""" import aqt.exporting aqt.exporting.ExportDialog(self, did=did) @@ -1014,6 +1085,7 @@ def onRefreshTimer(self): ########################################################################## def setupHooks(self): + """Adds onSchemadMod, onRemNotes and onOdueInvalid to their hooks""" addHook("modSchema", self.onSchemaMod) addHook("remNotes", self.onRemNotes) addHook("odueInvalid", self.onOdueInvalid) @@ -1048,6 +1120,11 @@ def onMpvIdle(self): ########################################################################## def onRemNotes(self, col, nids): + """Append (id, model id and fields) to the end of deleted.txt + + This is done for each id of nids. + This method is added to the hook remNotes; and executed on note deletion. + """ path = os.path.join(self.pm.profileFolder(), "deleted.txt") existed = os.path.exists(path) with open(path, "ab") as f: @@ -1064,6 +1141,7 @@ def onRemNotes(self, col, nids): ########################################################################## def onSchemaMod(self, arg): + """Ask the user whether they accept to do an action which will request a full reupload of the db""" return askUser(_("""\ The requested change will require a full upload of the database when \ you next synchronize your collection. If you have reviews or other changes \ @@ -1164,6 +1242,8 @@ def onStudyDeck(self): self.moveToState("overview") def onEmptyCards(self): + """Method called by Tools>Empty Cards...""" + self.progress.start(immediate=True) cids = self.col.emptyCids() if not cids: diff --git a/aqt/modelchooser.py b/aqt/modelchooser.py index 2eeae55cb51..5db821d8951 100644 --- a/aqt/modelchooser.py +++ b/aqt/modelchooser.py @@ -2,13 +2,23 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +"""The window allowing to choose a model. Either for a card to add, to +import notes, or to change the model of a card. + +""" + from aqt.qt import * from anki.hooks import addHook, remHook, runHook from aqt.utils import shortcut from anki.lang import _ class ModelChooser(QHBoxLayout): - + """ + label -- Whether this object corresponds to a button + (i.e. note importer/addcards, but not browser. + widget -- the button used to open this window. It contains the + name of the current model. + """ def __init__(self, mw, widget, label=True): QHBoxLayout.__init__(self) self.widget = widget @@ -44,6 +54,8 @@ def cleanup(self): remHook('reset', self.onReset) def onReset(self): + """Change the button's text so that it has the name of the current + model.""" self.updateModels() def show(self): @@ -57,6 +69,8 @@ def onEdit(self): aqt.models.Models(self.mw, self.widget) def onModelChange(self): + """Open Choose Note Type window""" + #Method called when we want to change the current model from aqt.studydeck import StudyDeck current = self.deck.models.current()['name'] # edit button @@ -79,4 +93,6 @@ def nameFunc(): self.mw.reset() def updateModels(self): + """Change the button's text so that it has the name of the current + model.""" self.models.setText(self.deck.models.current()['name']) diff --git a/aqt/models.py b/aqt/models.py index 0ccfe048634..d0ecfa333b0 100644 --- a/aqt/models.py +++ b/aqt/models.py @@ -1,6 +1,7 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +"""The window used to select a model. Either directly from note type manager in main. Or through a model chooser window.""" from aqt.qt import * from operator import itemgetter from aqt.utils import showInfo, askUser, getText, maybeHideClose, openHelp @@ -10,7 +11,22 @@ import collections from anki.lang import _, ngettext + class Models(QDialog): + """TODO + + An object of class Models contains: + mw -- The main window (?) + parent -- the window which opened the current window. By default + the main window + fromMain -- whether the window is opened from the main window. It + is used to check whether Fields... and Cards... buttons should be + added. + col -- the collection + mm -- the set of models of the colection + form -- TODO + models -- all models of the collection + """ def __init__(self, mw, parent=None, fromMain=False): self.mw = mw self.parent = parent or mw @@ -53,6 +69,7 @@ def setupModels(self): maybeHideClose(box) def onRename(self): + """Ask the user for a new name for the model. Save it""" txt = getText(_("New name:"), default=self.model['name']) if txt[1] and txt[0]: self.model['name'] = txt[0] @@ -74,6 +91,7 @@ def updateModelsList(self): self.form.modelsList.setCurrentRow(row) def modelChanged(self): + """Called if the selected model has changed, in order to change self.model""" if self.model: self.saveModel() idx = self.form.modelsList.currentRow() @@ -121,6 +139,7 @@ def onAdvanced(self): self.model['latexPost'] = str(frm.latexFooter.toPlainText()) def saveModel(self): + """Similar to "save the model" in anki/models.py""" self.mm.save(self.model) def _tmpNote(self): @@ -142,6 +161,7 @@ def onFields(self): FieldDialog(self.mw, n, parent=self) def onCards(self): + """Open the card preview(layout) window.""" from aqt.clayout import CardLayout n = self._tmpNote() CardLayout(self.mw, n, ord=0, parent=self, addMode=True) diff --git a/aqt/preferences.py b/aqt/preferences.py index 4a854c07203..0808d888d72 100644 --- a/aqt/preferences.py +++ b/aqt/preferences.py @@ -11,6 +11,10 @@ class Preferences(QDialog): + """ + startdate -- datetime where collection was created. Only in schedV1 + """ + def __init__(self, mw): QDialog.__init__(self, mw, Qt.Window) self.mw = mw @@ -225,4 +229,3 @@ def setupOptions(self): def updateOptions(self): self.prof['pastePNG'] = self.form.pastePNG.isChecked() - diff --git a/aqt/profiles.py b/aqt/profiles.py index 1fdf6fda6c5..d3da72f0d2b 100644 --- a/aqt/profiles.py +++ b/aqt/profiles.py @@ -251,15 +251,27 @@ def rename(self, name): ###################################################################### def profileFolder(self, create=True): + """The path to the folder of this profile. + + It is based on the base, and this profile name + This folder may not exists. + Create it only if does not exists and create is set to True""" path = os.path.join(self.base, self.name) if create: self._ensureExists(path) return path def addonFolder(self): + """The path to the add-on folder. + + Guarenteed to exists. + It is in base, not in profile""" return self._ensureExists(os.path.join(self.base, "addons21")) def backupFolder(self): + """The path to the backup folder. + + Guarenteed to exists""" return self._ensureExists( os.path.join(self.profileFolder(), "backups")) @@ -270,6 +282,7 @@ def collectionPath(self): ###################################################################### def _ensureExists(self, path): + """Create the path if it does not exists. Return the path""" if not os.path.exists(path): os.makedirs(path) return path @@ -285,6 +298,7 @@ def _setBaseFolder(self, cmdlineBase): self.ensureBaseExists() def _defaultBase(self): + """The folder containing every file related to anki's configuration. """ if isWin: from aqt.winpaths import get_appdata return os.path.join(get_appdata(), "Anki2") @@ -298,6 +312,14 @@ def _defaultBase(self): return os.path.join(dataDir, "Anki2") def _loadMeta(self): + """ + Copy prefs to prefs21.db if prefs exists only for 2.0 + Create a new profile and an error message if prefs21.db has a problem. + if no preference database exists, create it, and create a global profile in it using current meta. + put database of preferences in self.db + Put the _global preferences in self.meta + todo: explain call to _setDefaultLang + """ opath = os.path.join(self.base, "prefs.db") path = os.path.join(self.base, "prefs21.db") if os.path.exists(opath) and not os.path.exists(path): diff --git a/aqt/reviewer.py b/aqt/reviewer.py index 84c3be95a09..db9ce2129fc 100644 --- a/aqt/reviewer.py +++ b/aqt/reviewer.py @@ -428,7 +428,11 @@ def logGood(start, cnt, str, array): return givenElems, correctElems def correct(self, given, correct, showBad=True): - "Diff-corrects the typed-in answer." + """HTML code with a diff-corrects the typed-in answer. + + given -- answer given by the user + correct -- correct answer + showBad -- unused.""" givenElems, correctElems = self.tokenizeComparison(given, correct) def good(s): return ""+html.escape(s)+"" diff --git a/aqt/studydeck.py b/aqt/studydeck.py index 50c713deb6f..1d1583ef093 100644 --- a/aqt/studydeck.py +++ b/aqt/studydeck.py @@ -1,6 +1,9 @@ # Copyright: Ankitects Pty Ltd and contributors # -*- coding: utf-8 -*- # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +""" +The window obtained, from main window "Tools>Study deck" +""" from aqt.qt import * import aqt @@ -9,6 +12,13 @@ from anki.lang import _ class StudyDeck(QDialog): + """ + nameFunc -- names, a function or none. Called to compute the new name on reset. Currently, it's always the list of all decks or None. + origNames -- the names ? Either computed from nameFunc, or the list of all name of decks. + filt -- the text written in the window, to filter the decks shown + names -- set of decks to be shown. Subset of origname + name -- the selected deck, only after accepting + """ def __init__(self, mw, names=None, accept=None, title=None, help="studydeck", current=None, cancel=True, parent=None, dyn=False, buttons=None, geomKey="default"): @@ -75,6 +85,9 @@ def eventFilter(self, obj, evt): return False def redraw(self, filt, focus=None): + """filt -- text already entered + focus -- the name on which to focus if its in the list of deck names + """ self.filt = filt self.focus = focus self.names = [n for n in self.origNames if self._matches(n, filt)] @@ -89,6 +102,8 @@ def redraw(self, filt, focus=None): l.scrollToItem(l.item(idx), QAbstractItemView.PositionAtCenter) def _matches(self, name, filt): + """whether all words of filt, separated by spaces, appear in + name. This is how filter works.""" name = name.lower() filt = filt.lower() if not filt: @@ -99,6 +114,8 @@ def _matches(self, name, filt): return True def onReset(self): + """Recompute the set of decks, show the new deck with same + filter, same focus, but new name""" # model updated? if self.nameFunc: self.origNames = self.nameFunc() diff --git a/aqt/sync.py b/aqt/sync.py index 47322755a06..2f3c24b2a74 100644 --- a/aqt/sync.py +++ b/aqt/sync.py @@ -16,6 +16,10 @@ ###################################################################### class SyncManager(QObject): + """ + label -- The current action done by the synchronizez. Or in case f syncMsg, the first argument + """ + def __init__(self, mw, pm): QObject.__init__(self, mw) @@ -40,8 +44,10 @@ def _sync(self, auth=None): gc.collect() # create the thread, setup signals and start running t = self.thread = SyncThread( - self.pm.collectionPath(), self.pm.profile['syncKey'], - auth=auth, media=self.pm.profile['syncMedia'], + self.pm.collectionPath(), + self.pm.profile['syncKey'], + auth=auth, + media=self.pm.profile['syncMedia'], hostNum=self.pm.profile.get("hostNum"), ) t.event.connect(self.onEvent) @@ -151,6 +157,9 @@ def onEvent(self, evt, *args): self._updateLabel() def _rewriteError(self, err): + """Return an message explaining the error err. + + err -- is the traceback of an exception raised sometime during synchronization.""" if "Errno 61" in err: return _("""\ Couldn't connect to AnkiWeb. Please check your network connection \ @@ -197,6 +206,12 @@ def _rewriteError(self, err): return err def _getUserPass(self): + """The pair (user login (i.e. email), password). + + It creates a window asking for those informations. Returns + None if one of those value is not given. + + """ d = QDialog(self.mw) d.setWindowTitle("Anki") d.setWindowModality(Qt.WindowModal) @@ -237,6 +252,12 @@ def _getUserPass(self): return (u, p) def _confirmFullSync(self): + """Stop progess bar, ask user what to do with a message explaining + what are the choices and why it must be made. It then set + self.thread.fullSyncChoice to a word stating what the choice + is Then restart the progess bar. + + """ self.mw.progress.finish() if self.thread.localIsEmpty: diag = askUserDialog( @@ -273,11 +294,15 @@ def _confirmFullSync(self): self.mw.progress.start(immediate=True) def _clockOff(self): + """Show a message stating that there is more than 5 minutes of + difference between computer and server, thus sync can't be made.""" showWarning(_("""\ Syncing requires the clock on your computer to be set correctly. Please \ fix the clock and try again.""")) def _checkFailed(self): + """State to check database before syncing. (Actually, it's totally +useless if the database is downloaded the current will be deleted.)""" showWarning(_("""\ Your collection is in an inconsistent state. Please run Tools>\ Check Database, then sync again.""")) @@ -286,7 +311,15 @@ def _checkFailed(self): ###################################################################### class SyncThread(QThread): + """ + path -- the path of the database of the collection + syncMsg -- Initially the empty string. At the end of a successful non-full sync, before syncinc media, it becomes the value of the meta[TODO + hostNum -- the value of hostNum in the profile (i.e. a number which states the number of the ankiweb server which is used to synchronize) + hkey -- the value of SyncKey in the profile (i.e. a random string replacing the password) + auth -- the password; asked to the user (not saved) + media -- whether medias should be synchronized + """ event = pyqtSignal(str, str) def __init__(self, path, hkey, auth=None, media=True, hostNum=None): @@ -461,5 +494,3 @@ def _syncMedia(self): def fireEvent(self, cmd, arg=""): self.event.emit(cmd, arg) - - diff --git a/aqt/utils.py b/aqt/utils.py index 799fdf113d3..b2dae0cb76e 100644 --- a/aqt/utils.py +++ b/aqt/utils.py @@ -100,7 +100,15 @@ def onFinish(): def askUser(text, parent=None, help="", defaultno=False, msgfunc=None, \ title="Anki"): - "Show a yes/no question. Return true if yes." + """Show a yes/no question. Return true if yes. + + Text -- the text displayed to the user + parent -- Add this window as parent to the question + help -- An help message, occuring if the user click on the help button + defaultno -- whether the default answer is no + msgfunc -- The kind of QMessageBox. By default, a question + title -- title of the question window + """ if not parent: parent = aqt.mw.app.activeWindow() if not msgfunc: @@ -215,6 +223,7 @@ def getText(prompt, parent=None, help=None, edit=None, default="", return (str(d.l.text()), ret) def getOnlyText(*args, **kwargs): + """A text asked to the user, or the empty string.""" (s, r) = getText(*args, **kwargs) if r: return s @@ -313,6 +322,7 @@ def getSaveFile(parent, title, dir_description, key, ext, fname=None): return file def saveGeom(widget, key): + """Associate to the key the geometry of the widget""" key += "Geom" if isMac and widget.windowState() & Qt.WindowFullScreen: geom = None @@ -321,6 +331,11 @@ def saveGeom(widget, key): aqt.mw.pm.profile[key] = geom def restoreGeom(widget, key, offset=None, adjustSize=False): + """Gets from the collection profil the geometry associated to the widget named key. + + keywords parameter: + offset -- whether a mac window must be resized, assuming qtminor >6 ??? + adjustSize -- Whether to call widget.adjustSize if the key does not belongs in the database """ key += "Geom" if aqt.mw.pm.profile.get(key): widget.restoreGeometry(aqt.mw.pm.profile[key]) @@ -387,6 +402,10 @@ def restoreHeader(widget, key): widget.restoreState(aqt.mw.pm.profile[key]) def mungeQA(col, txt): + """txt, without its sound, and Replace local image url by replacing + special character by the escape %xx + + """ txt = col.media.escapeImages(txt) txt = stripSounds(txt) return txt @@ -399,11 +418,13 @@ def openFolder(path): QDesktopServices.openUrl(QUrl("file://" + path)) def shortcut(key): + """On mac: ctrl is replaced by command. Otherwise identity""" if isMac: return re.sub("(?i)ctrl", "Command", key) return key def maybeHideClose(bbox): + """On mac only, remove the bbox's close buton.""" if isMac: b = bbox.button(QDialogButtonBox.Close) if b: @@ -428,6 +449,8 @@ def downArrow(): _tooltipLabel = None def tooltip(msg, period=3000, parent=None): + """Show a small yellow box for period milliseconds, containing the +text msg.""" global _tooltipTimer, _tooltipLabel class CustomLabel(QLabel): silentlyClose = True diff --git a/aqt/webview.py b/aqt/webview.py index ca3b2976eca..3ae9486fdb8 100644 --- a/aqt/webview.py +++ b/aqt/webview.py @@ -48,9 +48,9 @@ def cmd(self, str): cb(JSON.parse(res)); } } - + channel.objects.py.cmd(arg, resultCB); - return false; + return false; } pycmd("domDone"); }); @@ -265,12 +265,12 @@ def stdHtml(self, body, css=None, js=None, head=""): button:active, button:active:hover { background-color: %(color_hl)s; color: %(color_hl_txt)s;} /* Input field focus outline */ textarea:focus, input:focus, input[type]:focus, .uneditable-input:focus, -div[contenteditable="true"]:focus { +div[contenteditable="true"]:focus { outline: 0 none; border-color: %(color_hl)s; }""" % {"family": family, "color_btn": color_btn, "color_hl": color_hl, "color_hl_txt": color_hl_txt} - + csstxt = "\n".join([self.bundledCSS("webview.css")]+ [self.bundledCSS(fname) for fname in css]) jstxt = "\n".join([self.bundledScript("webview.js")]+ @@ -287,7 +287,7 @@ def stdHtml(self, body, css=None, js=None, head=""): body {{ zoom: {}; background: {}; {} }} {} - + {} @@ -308,12 +308,25 @@ def bundledCSS(self, fname): return '' % self.webBundlePath(fname) def eval(self, js): + """Add the script `js` to the list of event to execute. Execute it now + if possible. + + """ self.evalWithCallback(js, None) def evalWithCallback(self, js, cb): + """Add the js script to the list of actions to perform. Perform it if + possible. Call cb with the last result evaluated by + javascript. + + """ self._queueAction("eval", js, cb) def _evalWithCallback(self, js, cb): + """Perform the script `js` in the web pag. Call cb on its last result + if its truthy. + + """ if cb: def handler(val): if self._shouldIgnoreWebEvent(): @@ -325,10 +338,15 @@ def handler(val): self.page().runJavaScript(js) def _queueAction(self, name, *args): + """Add action name with arguments args to the list of action to + perform. Try running those actions. + + name -- eval or setHtml""" self._pendingActions.append((name, args)) self._maybeRunActions() def _maybeRunActions(self): + """Do the actions of pending actions, while _domDone""" while self._pendingActions and self._domDone: name, args = self._pendingActions.pop(0) diff --git a/documentation/hooks.md b/documentation/hooks.md index aff3b8ec719..fbc0c9d6f12 100644 --- a/documentation/hooks.md +++ b/documentation/hooks.md @@ -419,7 +419,15 @@ anki.main.AnkiQt._reviewCleanup assuming the new state is neither Contains anki.latex.mungeQA. A method which generate HTML which show an image which contains the compilation result of the LaTeX given in input. -Called from anki.collection._Collection._renderQA +Called from anki.collection._Collection._renderQA. + +Arguments are: +* the card template, with fields replaced by their values. +* "q" or "a" +* map sending fields to their value (and special name) +* the model object +* 8-tuple of data +* the collection ###### prepareQA Called from aqt.reviewer.Reviewer._showQuestion, with the text of the From 9f361369df45c90414f2ef84b16f9870c0090029 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Sat, 15 Jun 2019 15:53:35 +0200 Subject: [PATCH 24/68] commenting js --- aqt/editor.py | 2 +- web/editor.js | 116 +++++++++++++++++++++++++++++++++++++++++------- web/reviewer.js | 2 + 3 files changed, 103 insertions(+), 17 deletions(-) diff --git a/aqt/editor.py b/aqt/editor.py index 1a0c15421ca..da5802ac15e 100644 --- a/aqt/editor.py +++ b/aqt/editor.py @@ -919,7 +919,7 @@ def dropEvent(self, evt): mime = evt.mimeData() if evt.source() and mime.hasHtml(): - # don't filter html from other fields + # don't filterhtml from other fields html, internal = mime.html(), True else: html, internal = self._processMime(mime) diff --git a/web/editor.js b/web/editor.js index aaaf686281b..5ddfb1ee2af 100644 --- a/web/editor.js +++ b/web/editor.js @@ -1,11 +1,13 @@ /* Copyright: Ankitects Pty Ltd and contributors * License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */ -var currentField = null; -var changeTimer = null; -var dropTarget = null; -var currentNoteId = null; +var currentField = null; // The html field which was last selected (or on which something was dropped. I.e. the field having the focus) +var changeTimer = null; // A setTimeout eevnt, to be executed if + // nothing else occurs. It changes the button highlightment, and save. +var dropTarget = null; //The last field on which something was dropped. +var currentNoteId = null; // A note id, as given by python. +/* Methods which replace {d}, with d a number, by the d-th argument.*/ String.prototype.format = function () { var args = arguments; return this.replace(/\{\d+\}/g, function (m) { @@ -14,10 +16,15 @@ String.prototype.format = function () { }; function setFGButton(col) { + /* Change the «foreground coulor» button to col*/ $("#forecolor")[0].style.backgroundColor = col; } + function saveNow(keepFocus) { + /* Save data. With the "blur" command if keepFocus is falsy, otherwise with "key" command. + + if keepFocus is falsy, remove the focus.*/ if (!currentField) { return; } @@ -33,6 +40,9 @@ function saveNow(keepFocus) { } function triggerKeyTimer() { + /*In .6 seconds, update which buttons are highlighted, and save the content. + This way, if you type quickly (i.e. less than half a second by key), then it's not always saved. + */ clearChangeTimer(); changeTimer = setTimeout(function () { updateButtonState(); @@ -41,6 +51,16 @@ function triggerKeyTimer() { } function onKey() { + /* Executed either if a key is pressed or when mouse up in the + * field. + + Esc clears focus for the dialog to close + shift+tab change the focus to previous field on macintel (it's already the default otherwise) + + If no other action is done in .6 seconds, tell Python what change did occur + */ + + // esc clears focus, allowing dialog to close if (window.event.which === 27) { currentField.blur(); @@ -57,6 +77,8 @@ function onKey() { } function insertNewline() { + /* Replace the selected text by a \n character. May be multiple + * \n, so that the user see the difference.*/ if (!inPreEnvironment()) { setFormat("insertText", "\n"); return; @@ -83,14 +105,20 @@ function insertNewline() { // is the cursor in an environment that respects whitespace? function inPreEnvironment() { - var n = window.getSelection().anchorNode; - if (n.nodeType === 3) { + var selection = window.getSelection(); // The selected part of the text/where the cursor is + var n = selection.anchorNode; // where the text selected begin + if (n.nodeType === 3) {//3 is Node.TEXT_NODE n = n.parentNode; } - return window.getComputedStyle(n).whiteSpace.startsWith("pre"); + var css = window.getComputedStyle(n); + return css.whiteSpace.startsWith("pre"); } function onInput() { + /*Ensure that current field is not empty. If it were,
is + * inserted instead so that the field looks like a text field + + This is checked on every input; i.e. when the text change.*/ // empty field? if (currentField.innerHTML === "") { currentField.innerHTML = "
"; @@ -101,6 +129,8 @@ function onInput() { } function updateButtonState() { + /* Apply css class highlighted (i.e. underline), the style buttons + * which are applied to the last selected text */ var buts = ["bold", "italic", "underline", "superscript", "subscript"]; for (var i = 0; i < buts.length; i++) { var name = buts[i]; @@ -124,6 +154,9 @@ function toggleEditorButton(buttonid) { } function setFormat(cmd, arg, nosave) { + /* Execute command cmd with argument arg on the currently selected text. nosave determines whether the text must be saved after that. + + cmd is a command which change the text of a field*/ document.execCommand(cmd, false, arg); if (!nosave) { saveField('key'); @@ -132,6 +165,7 @@ function setFormat(cmd, arg, nosave) { } function clearChangeTimer() { + /* Cancel the fact that buttons must be changed and content saved */ if (changeTimer) { clearTimeout(changeTimer); changeTimer = null; @@ -139,6 +173,15 @@ function clearChangeTimer() { } function onFocus(elem) { + /* + Called when focus is set to the field `elem`. + + If the field is not changed, nothing occurs. + Otherwise, set currentField value, warns python of it. + Change buttons. + If the change is note made by mouse, then move caret to end of field, and move the window to show the field. + + */ if (currentField === elem) { // anki window refocused; current element unchanged return; @@ -169,6 +212,7 @@ function onFocus(elem) { } function focusField(n) { + /*Put focus in field number n*/ if (n === null) { return; } @@ -176,6 +220,9 @@ function focusField(n) { } function focusPrevious() { + /*Focus on the field before current field. + Only required on mac, otherwise it occurs by default + */ if (!currentField) { return; } @@ -202,6 +249,7 @@ function makeDropTargetCurrent() { } function onPaste(elem) { + /*Tells Python to deal with pasting the data*/ pycmd("paste"); window.event.preventDefault(); } @@ -216,8 +264,12 @@ function caretToEnd() { } function onBlur() { - if (!currentField) { - return; + /*Tells python that it must save. Either by key if current field + is still active. Otherwise by blur. If current field is not + active, then disable buttons and state that there are no current + fields */ + if (!currentField) { + return; } if (document.activeElement === currentField) { @@ -231,6 +283,10 @@ function onBlur() { } function saveField(type) { + /* Send to python an information about what just occured, on which + * field, which note (id) and with what value in the field. + + Event may be "blur" when focus is lost. Or "key" otherwise*/ clearChangeTimer(); if (!currentField) { // no field has been focused yet @@ -257,8 +313,8 @@ function enableButtons() { $("button.linkb").prop("disabled", false); } -// disable the buttons if a field is not currently focused function maybeDisableButtons() { + /*disable the buttons if a field is not currently focused*/ if (!document.activeElement || document.activeElement.className !== "field") { disableButtons(); } else { @@ -267,6 +323,7 @@ function maybeDisableButtons() { } function wrap(front, back) { + /* todo*/ if (currentField.dir === "rtl") { front = "‫" + front + "‬"; back = "‫" + back + "‬"; @@ -289,24 +346,29 @@ function wrap(front, back) { } function onCutOrCopy() { + /*Ask python to deals with cut or copy*/ pycmd("cutOrCopy"); return true; } function setFields(fields) { + /*Replace #fields by the HTML to show the list of fields to edit. + Potentially change buttons + + fields -- a list of fields, as (name of the field, current value) */ var txt = ""; for (var i = 0; i < fields.length; i++) { - var n = fields[i][0]; - var f = fields[i][1]; - if (!f) { - f = "
"; + var fieldName = fields[i][0]; + var fieldValue = fields[i][1]; + if (!fieldValue) { + fieldValue = "
"; } - txt += "{0}".format(n); + txt += "{0}".format(fieldName); txt += "
" + txt + ""); @@ -314,12 +376,16 @@ function setFields(fields) { } function setBackgrounds(cols) { + /*Change the backgroud color of field i to cols[i]. + + Used to warn when first field is a duplicate*/ for (var i = 0; i < cols.length; i++) { $("#f" + i).css("background", cols[i]); } } function setFonts(fonts) { + /* set fonts family and size according of the i-th field according to fonts[i]*/ for (var i = 0; i < fonts.length; i++) { var n = $("#f" + i); n.css("font-family", fonts[i][0]) @@ -329,18 +395,22 @@ function setFonts(fonts) { } function setNoteId(id) { + /*Change currentNoteId to id*/ currentNoteId = id; } function showDupes() { + /*Show the message stating that they are dupes, and tells to show them.*/ $("#dupes").show(); } function hideDupes() { + /*Hide the message stating that they are dupes, and tells to show them.*/ $("#dupes").hide(); } var pasteHTML = function (html, internal, extendedMode) { + /* TODO */ html = filterHTML(html, internal, extendedMode); if (html !== "") { // remove trailing
in empty field @@ -352,6 +422,7 @@ var pasteHTML = function (html, internal, extendedMode) { }; var filterHTML = function (html, internal, extendedMode) { + /* used only by pasting. TODO */ // wrap it in as we aren't allowed to change top level elements var top = $.parseHTML("" + html + "")[0]; if (internal) { @@ -370,6 +441,11 @@ var filterHTML = function (html, internal, extendedMode) { return outHtml; }; +/* dict, associating to each tag the list of possible attributes. +Extended contains all tags from basic. + +Basic tags can always be copy/pasted. In extended mode, extended tags can be pasted + */ var allowedTagsBasic = {}; var allowedTagsExtended = {}; @@ -398,6 +474,7 @@ Object.assign(allowedTagsExtended, allowedTagsBasic); // filtering from another field var filterInternalNode = function (node) { + /* used only by pasting. TODO */ if (node.style) { node.style.removeProperty("background-color"); node.style.removeProperty("font-size"); @@ -411,6 +488,7 @@ var filterInternalNode = function (node) { // filtering from external sources var filterNode = function (node, extendedMode) { + /* used only by pasting. TODO */ // text node? if (node.nodeType === 3) { return; @@ -461,11 +539,17 @@ var filterNode = function (node, extendedMode) { }; var adjustFieldsTopMargin = function() { + /* add margin 8px to the top of buttons. + + */ var topHeight = $("#topbuts").height(); var margin = topHeight + 8; document.getElementById("fields").style.marginTop = margin + "px"; }; +/*1 when mouseDown, +0 on mouseUp. (Unless there are multiple mouse. Instead, it's the number of mouse with mouseDown) +*/ var mouseDown = 0; $(function () { diff --git a/web/reviewer.js b/web/reviewer.js index 9fdec7ccff9..d0f7b5b9987 100644 --- a/web/reviewer.js +++ b/web/reviewer.js @@ -73,6 +73,8 @@ function _showQuestion(q, bodyclass) { } function _showAnswer(a, bodyclass) { + /*Change the class name to bodyclass. Move to the first #answer in + * the html.*/ _updateQA(a, aFade, function() { if (bodyclass) { // when previewing From eb51c3307e7f29e3c8d492e06154552a9f1ef9e9 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Sat, 6 Jul 2019 19:03:46 +0200 Subject: [PATCH 25/68] commenting import CSV --- anki/importing/csvfile.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/anki/importing/csvfile.py b/anki/importing/csvfile.py index 28ddef3723b..5679e2a390c 100644 --- a/anki/importing/csvfile.py +++ b/anki/importing/csvfile.py @@ -55,21 +55,31 @@ def foreignNotes(self): return notes def open(self): - "Parse the top line and determine the pattern and number of fields." + "Same as cacheFile" # load & look for the right pattern self.cacheFile() def cacheFile(self): - "Read file into self.lines if not already there." + """Read file into self.lines if not already there. + set openFile for the remaining. + """ if not self.fileobj: self.openFile() def openFile(self): + """Put: + in data: all lines not starting with #, and not tags + in tags: the tags, separated by space, assuming first line which is not a comment start with "tags:". + Set CSV reader, or delimiter if csv can't be guessed. + set numFields to the number of fields of the first non empty line + set mapping to initial mapping. + """ self.dialect = None self.fileobj = open(self.file, "r", encoding='utf-8-sig') self.data = self.fileobj.read() def sub(s): return re.sub(r"^\#.*$", "__comment", s) + #set of lines not starting with # self.data = [sub(x)+"\n" for x in self.data.split("\n") if sub(x) != "__comment"] if self.data: if self.data[0].startswith("tags:"): @@ -81,6 +91,11 @@ def sub(s): raise Exception("unknownFormat") def updateDelimiter(self): + """If possible, set CSV dialect, as guessed by CSV library. + Otherwise, set delimiter to "\t", ";", "," if they are in the first line. To " " otherwise + set numFields to the number of fields of the first non empty line + set mapping to initial mapping. + """ def err(): raise Exception("unknownFormat") self.dialect = None From 2ea7e3765da0708064c7582357a9d659418d3b89 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Tue, 9 Jul 2019 18:06:09 +0200 Subject: [PATCH 26/68] Factorizing both schedulers --- anki/bothSched.py | 855 ++++++++++++++++++++++++++++++++++++++++++ anki/sched.py | 796 +-------------------------------------- anki/schedv2.py | 676 +-------------------------------- tests/test_schedv1.py | 8 +- tests/test_schedv2.py | 8 +- 5 files changed, 907 insertions(+), 1436 deletions(-) create mode 100644 anki/bothSched.py diff --git a/anki/bothSched.py b/anki/bothSched.py new file mode 100644 index 00000000000..a5900becfd1 --- /dev/null +++ b/anki/bothSched.py @@ -0,0 +1,855 @@ +# -*- coding: utf-8 -*- +# Copyright: Ankitects Pty Ltd and contributors +# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +import time +import datetime +import random +import itertools +from operator import itemgetter +from heapq import * + +#from anki.cards import Card +from anki.utils import ids2str, intTime, fmtTimeSpan +from anki.lang import _ +from anki.consts import * +from anki.hooks import runHook + +# it uses the following elements from anki.consts +# card types: 0=new, 1=lrn, 2=rev, 3=relrn +# queue types: 0=new, 1=(re)lrn, 2=rev, 3=day (re)lrn, +# 4=preview, -1=suspended, -2=sibling buried, -3=manually buried +# revlog types: 0=lrn, 1=rev, 2=relrn, 3=early review +# positive revlog intervals are in days (rev), negative in seconds (lrn) +# odue/odid store original due/did when cards moved to filtered deck + +class BothScheduler: + """ + today -- difference between the last time scheduler is seen and creation of the collection. + dayCutoff -- The timestamp of when today end. + reportLimit -- the maximal number to show in main windows + lrnCount -- The number of cards in learning in selected decks + revCount -- number of cards to review today in selected decks + newCount -- number of new cards to see today in selected decks + _lrnDids, _revDids, _newDids -- a copy of the set of active decks where decks with no card to see today are removed. + _newQueue, _lrnQueue, _revQueue -- list of ids of cards in the queue new, lrn and rev. At most queue limit (i.e. 50) + queueLimit -- maximum number of cards to queue simultaneously. Always 50 unless changed by an addon. + _lrnDayQueue -- todo + newCardModulus -- each card in position 0 Modulo newCardModulus is a new card. Or it is 0 if new cards are not mixed with reviews. + -- _haveQueues -- whether the number of cards to see today for current decks have been set. + """ + haveCustomStudy = True + _spreadRev = True + _burySiblingsOnAnswer = True + + def __init__(self, col): + self.col = col + self.queueLimit = 50 + self.reportLimit = 1000 + self.reps = 0 + self.today = None + self._haveQueues = False + self._updateCutoff() + + def getCard(self): + "Pop the next card from the queue. None if finished." + self._checkDay() + if not self._haveQueues: + self.reset() + card = self._getCard() + if card: + self.col.log(card) + if not self._burySiblingsOnAnswer: + self._burySiblings(card) + self.reps += 1 + card.startTimer() + return card + + def reset(self): + """ + Deal with the fact that it's potentially a new day. + Reset number of learning, review, new cards according to current decks + empty queues. Set haveQueues to true + """ + self._updateCutoff() + self._resetLrn() + self._resetRev() + self._resetNew() + self._haveQueues = True + + def dueForecast(self, days=7): + "Return counts over next DAYS. Includes today." + daysd = dict(self.col.db.all(f""" +select due, count() from cards +where did in %s and queue = {QUEUE_REV} +and due between ? and ? +group by due +order by due""" % (self._deckLimit()), + self.today, + self.today+days-1)) + for d in range(days): + d = self.today+d + if d not in daysd: + daysd[d] = 0 + # return in sorted order + ret = [x[1] for x in sorted(daysd.items())] + return ret + + # Rev/lrn/time daily stats + ########################################################################## + + def _updateStats(self, card, type, cnt=1): + """Change the number of review/new/learn cards to see today in card's + deck, and in all of its ancestors. The number of card to see + is decreased by cnt. + + if type is time, it adds instead the time taken in this card + to this decks and all of its ancestors. + """ + key = type+"Today" + for g in ([self.col.decks.get(card.did)] + + self.col.decks.parents(card.did)): + # add + g[key][1] += cnt + self.col.decks.save(g) + + def extendLimits(self, new, rev): + """Decrease the limit of new/rev card to see today to this deck, its + ancestors and all of its descendant, by new/rev. + + This number is called from aqt.customstudy.CustomStudy.accept, with the number of card to study today. + """ + cur = self.col.decks.current() + parents = self.col.decks.parents(cur['id']) + children = [self.col.decks.get(did) for (name, did) in + self.col.decks.children(cur['id'])] + for g in [cur] + parents + children: + # add + g['newToday'][1] -= new + g['revToday'][1] -= rev + self.col.decks.save(g) + + def _walkingCount(self, limFn=None, cntFn=None): + """The sum of cntFn applied to each active deck. + + limFn -- function which associate to each deck obejct the maximum number of card to consider + cntFn -- function which, given a deck id and a limit, return a number of card at most equal to this limit.""" + tot = 0 + pcounts = {}# Associate from each id of a parent deck p, the maximal number of cards of deck p which can be seen, minus the card found for its descendant already considered + # for each of the active decks + nameMap = self.col.decks.nameMap() + for did in self.col.decks.active(): + # early alphas were setting the active ids as a str + did = int(did) + # get the individual deck's limit + lim = limFn(self.col.decks.get(did)) + if not lim: + continue + # check the parents + parents = self.col.decks.parents(did, nameMap) + for p in parents: + # add if missing + if p['id'] not in pcounts: + pcounts[p['id']] = limFn(p) + # take minimum of child and parent + lim = min(pcounts[p['id']], lim) + # see how many cards we actually have + cnt = cntFn(did, lim) + # if non-zero, decrement from parent counts + for p in parents: + pcounts[p['id']] -= cnt + # we may also be a parent + pcounts[did] = lim - cnt + # and add to running total + tot += cnt + return tot + + # Deck list + ########################################################################## + + def _groupChildren(self, grps): + """[subdeck name without parent parts, + did, rev, lrn, new (counting subdecks) + [recursively the same things for the children]] + + Keyword arguments: + grps -- [deckname, did, rev, lrn, new] + """ + # first, split the group names into components + for g in grps: + g[0] = g[0].split("::") + # and sort based on those components + grps.sort(key=itemgetter(0)) + # then run main function + return self._groupChildrenMain(grps) + + # New cards + ########################################################################## + + def _resetNewCount(self): + """Set newCount to the counter of new cards for the active decks.""" + # Number of card in deck did, at most lim + def cntFn(did, lim): + ret = self.col.db.scalar(f""" +select count() from (select 1 from cards where +did = ? and queue = {QUEUE_NEW_CRAM} limit ?)""", did, lim) + return ret + self.newCount = self._walkingCount(self._deckNewLimitSingle, cntFn) + + def _resetNew(self): + """Set newCount, newDids, newCardModulus. Empty newQueue. """ + self._resetNewCount() + self._newDids = self.col.decks.active()[:] + self._newQueue = [] + self._updateNewCardRatio() + + def _fillNew(self): + """Whether there are new card in current decks to see today. + + If it is the case that the _newQueue is not empty + """ + if self._newQueue: + return True + if not self.newCount: + return False + while self._newDids: + did = self._newDids[0] + lim = min(self.queueLimit, self._deckNewLimit(did)) + if lim: + # fill the queue with the current did + self._newQueue = self.col.db.list(f""" + select id from cards where did = ? and queue = {QUEUE_NEW_CRAM} order by due,ord limit ?""" , did, lim) + if self._newQueue: + self._newQueue.reverse() + return True + # nothing left in the deck; move to next + self._newDids.pop(0) + if self.newCount: + # if we didn't get a card but the count is non-zero, + # we need to check again for any cards that were + # removed from the queue but not buried + self._resetNew() + return self._fillNew() + + def _getNewCard(self): + if self._fillNew(): + self.newCount -= 1 + return self.col.getCard(self._newQueue.pop()) + + def _updateNewCardRatio(self): + """set newCardModulus so that new cards are regularly mixed with review cards. At least 2. + Only if new and review should be mixed""" + if self.col.conf['newSpread'] == NEW_CARDS_DISTRIBUTE: + if self.newCount: + self.newCardModulus = ( + (self.newCount + self.revCount) // self.newCount) + # if there are cards to review, ensure modulo >= 2 + if self.revCount: + self.newCardModulus = max(2, self.newCardModulus) + return + self.newCardModulus = 0 + + def _timeForNewCard(self): + "True if it's time to display a new card when distributing." + if not self.newCount: + return False + if self.col.conf['newSpread'] == NEW_CARDS_LAST: + return False + elif self.col.conf['newSpread'] == NEW_CARDS_FIRST: + return True + elif self.newCardModulus: + return self.reps and self.reps % self.newCardModulus == 0 + + def _deckNewLimit(self, did, fn=None): + if not fn: + fn = self._deckNewLimitSingle + sel = self.col.decks.get(did) + lim = -1 + # for the deck and each of its parents + for g in [sel] + self.col.decks.parents(did): + rem = fn(g) + if lim == -1: + lim = rem + else: + lim = min(rem, lim) + return lim + + def _newForDeck(self, did, lim): + """The minimum between the number of new cards in this deck lim and self.reportLimit. + + keyword arguments: + did -- id of a deck + lim -- an upper bound for the returned number (in practice, the number of new cards to see by day for the deck's option) """ + if not lim: + return 0 + lim = min(lim, self.reportLimit) + return self.col.db.scalar(f""" +select count() from +(select 1 from cards where did = ? and queue = {QUEUE_NEW_CRAM} limit ?)""", did, lim) + + def totalNewForCurrentDeck(self): + return self.col.db.scalar( + f""" +select count() from cards where id in ( +select id from cards where did in %s and queue = {QUEUE_NEW_CRAM} limit ?)""" + % ids2str(self.col.decks.active(), self.reportLimit)) + + # Learning queues + ########################################################################## + + def _resetLrn(self): + """Set lrnCount and _lrnDids. Empty _lrnQueue, lrnDayQueu.""" + self._resetLrnCount() + self._lrnQueue = [] + self._lrnDayQueue = [] + self._lrnDids = self.col.decks.active()[:] + + # sub-day learning + def _fillLrn(self, cutoff, queueIn): + if not self.lrnCount: + return False + if self._lrnQueue: + return True + self._lrnQueue = self.col.db.all(f""" +select due, id from cards where +did in %s and queue in {queueIn} and due < :lim +limit %d""" % (self._deckLimit(), self.reportLimit), lim=self.dayCutoff) + # as it arrives sorted by did first, we need to sort it + self._lrnQueue.sort() + return self._lrnQueue + + # daily learning + def _fillLrnDay(self): + if not self.lrnCount: + return False + if self._lrnDayQueue: + return True + while self._lrnDids: + did = self._lrnDids[0] + # fill the queue with the current did + self._lrnDayQueue = self.col.db.list(f""" +select id from cards where +did = ? and queue = {QUEUE_DAY_LRN} and due <= ? limit ?""", + did, self.today, self.queueLimit) + if self._lrnDayQueue: + # order + r = random.Random() + r.seed(self.today) + r.shuffle(self._lrnDayQueue) + # is the current did empty? + if len(self._lrnDayQueue) < self.queueLimit: + self._lrnDids.pop(0) + return True + # nothing left in the deck; move to next + self._lrnDids.pop(0) + + def _getLrnDayCard(self): + if self._fillLrnDay(): + self.lrnCount -= 1 + return self.col.getCard(self._lrnDayQueue.pop()) + + def _leftToday(self, delays, left, now=None): + """The number of the last ```left``` steps that can be completed + before the day cutoff. Assuming the first step is done + ```now```. + + delays -- the list of delays + left -- the number of step to consider (at the end of the + list) + now -- the time at which the first step is done. + """ + if not now: + now = intTime() + delays = delays[-left:] + ok = 0 + for i in range(len(delays)): + now += delays[i]*60 + if now > self.dayCutoff: + break + ok = i + return ok+1 + + def _delayForGrade(self, conf, left): + """The number of second for the delay until the next time the card can + be reviewed. Assuming the number of left steps is left, + according to configuration conf + + """ + left = left % 1000 + try: + delay = conf['delays'][-left] + except IndexError: + if conf['delays']: + delay = conf['delays'][0] + else: + # user deleted final step; use dummy value + delay = 1 + return delay*60 + + def _rescheduleNew(self, card, conf, early): + """Reschedule a new card that's graduated for the first time. + + Set its factor according to conf. + Set its interval. If it's lapsed in dynamic deck, use + _dynIvlBoost. + Otherwise, the interval is found in conf['ints'][1 if early + else 0]. + Change due date according to the interval. + Put initial factor. + """ + card.ivl = self._graduatingIvl(card, conf, early) + card.due = self.today+card.ivl + card.factor = conf['initialFactor'] + + def _logLrn(self, card, ease, conf, leaving, type, lastLeft): + lastIvl = -(self._delayForGrade(conf, lastLeft)) + ivl = card.ivl if leaving else -(self._delayForGrade(conf, card.left)) + def log(): + self.col.db.execute( + "insert into revlog values (?,?,?,?,?,?,?,?,?)", + int(time.time()*1000), card.id, self.col.usn(), ease, + ivl, lastIvl, card.factor, card.timeTaken(), type) + try: + log() + except: + # duplicate pk; retry in 10ms + time.sleep(0.01) + log() + + # Reviews + ########################################################################## + + def _resetRev(self): + """Set revCount, empty _revQueue, _revDids""" + self._resetRevCount() + self._revQueue = [] + + def _getRevCard(self): + if self._fillRev(): + self.revCount -= 1 + return self.col.getCard(self._revQueue.pop()) + + def totalRevForCurrentDeck(self): + return self.col.db.scalar( + """ +select count() from cards where id in ( +select id from cards where did in %s and queue = {QUEUE_REV} and due <= ? limit ?)""" + % ids2str(self.col.decks.active()), self.today, self.reportLimit) + + # Answering a review card + ########################################################################## + + # Interval management + ########################################################################## + + def _fuzzedIvl(self, ivl): + """Return a randomly chosen number of day for the interval, + not far from ivl. + + See ../documentation/computing_intervals for a clearer version + of this documentation + """ + min, max = self._fuzzIvlRange(ivl) + return random.randint(min, max) + + def _fuzzIvlRange(self, ivl): + """Return an increasing pair of numbers. The new interval will be a + number randomly selected between the first and the second + element. + + See ../documentation/computing_intervals for a clearer version + of this documentation + + """ + if ivl < 2: + return [1, 1] + elif ivl == 2: + return [2, 3] + elif ivl < 7: + fuzz = int(ivl*0.25) + elif ivl < 30: + fuzz = max(2, int(ivl*0.15)) + else: + fuzz = max(4, int(ivl*0.05)) + # fuzz at least a day + fuzz = max(fuzz, 1) + return [ivl-fuzz, ivl+fuzz] + + def _daysLate(self, card): + "Number of days later than scheduled." + due = card.odue if card.odid else card.due + return max(0, self.today - due) + + # Dynamic deck handling + ########################################################################## + + def rebuildDyn(self, did=None): + "Rebuild a dynamic deck." + did = did or self.col.decks.selected() + deck = self.col.decks.get(did) + assert deck['dyn'] + # move any existing cards back first, then fill + self.emptyDyn(did) + ids = self._fillDyn(deck) + if not ids: + return + # and change to our new deck + self.col.decks.select(did) + return ids + + def remFromDyn(self, cids): + self.emptyDyn(None, "id in %s and odid" % ids2str(cids)) + + def _dynOrder(self, o, l): + if o == DYN_OLDEST: + t = "(select max(id) from revlog where cid=c.id)" + elif o == DYN_RANDOM: + t = "random()" + elif o == DYN_SMALLINT: + t = "ivl" + elif o == DYN_BIGINT: + t = "ivl desc" + elif o == DYN_LAPSES: + t = "lapses desc" + elif o == DYN_ADDED: + t = "n.id" + elif o == DYN_REVADDED: + t = "n.id desc" + elif o == DYN_DUE: + t = "c.due" + elif o == DYN_DUEPRIORITY: + t = f"(case when queue={QUEUE_REV} and due <= %d then (ivl / cast(%d-due+0.001 as real)) else 100000+due end)" % (self.today, self.today) + else: + # if we don't understand the term, default to due order + t = "c.due" + return t + " limit %d" % l + + + # Tools + ########################################################################## + + def _cardConf(self, card): + """The configuration of this card's deck. See decks.py + documentation to read more about them.""" + return self.col.decks.confForDid(card.did) + + def _newConf(self, card): + """The configuration for "new" of this card's deck.See decks.py + documentation to read more about them. + + """ + conf = self._cardConf(card) + # normal deck + if not card.odid: + return conf['new'] + # dynamic deck; override some attributes, use original deck for others + oconf = self.col.decks.confForDid(card.odid) + return dict( + # original deck + ints=oconf['new']['ints'], + initialFactor=oconf['new']['initialFactor'], + bury=oconf['new'].get("bury", True), + delays=self._delays(conf, oconf, "new"), + # overrides + separate=conf['separate'], + order=NEW_CARDS_DUE, + perDay=self.reportLimit + ) + + def _lapseConf(self, card): + """The configuration for "lapse" of this card's deck.See decks.py + documentation to read more about them. + + """ + conf = self._cardConf(card) + # normal deck + if not card.odid: + return conf['lapse'] + # dynamic deck; override some attributes, use original deck for others + oconf = self.col.decks.confForDid(card.odid) + delays = self._delays(conf, oconf, "lapse") + return dict( + # original deck + minInt=oconf['lapse']['minInt'], + leechFails=oconf['lapse']['leechFails'], + leechAction=oconf['lapse']['leechAction'], + mult=oconf['lapse']['mult'], + delays=delays, + # overrides + resched=conf['resched'], + ) + + def _revConf(self, card): + """The configuration for "review" of this card's deck.See decks.py + documentation to read more about them. + + """ + conf = self._cardConf(card) + # normal deck + if not card.odid: + return conf['rev'] + # dynamic deck + return self.col.decks.confForDid(card.odid)['rev'] + + def _deckLimit(self): + """The list of active decks, as comma separated parenthesized + string""" + return ids2str(self.col.decks.active()) + + # Daily cutoff + ########################################################################## + + def _checkDay(self): + # check if the day has rolled over + if time.time() > self.dayCutoff: + self.reset() + + # Deck finished state + ########################################################################## + + def finishedMsg(self): + return (""+_( + "Congratulations! You have finished this deck for now.")+ + "

" + self._nextDueMsg()) + + def _nextDueMsg(self): + line = [] + # the new line replacements are so we don't break translations + # in a point release + if self.revDue(): + line.append(_("""\ +Today's review limit has been reached, but there are still cards +waiting to be reviewed. For optimum memory, consider increasing +the daily limit in the options.""").replace("\n", " ")) + if self.newDue(): + line.append(_("""\ +There are more new cards available, but the daily limit has been +reached. You can increase the limit in the options, but please +bear in mind that the more new cards you introduce, the higher +your short-term review workload will become.""").replace("\n", " ")) + if self.haveBuried(): + if self.haveCustomStudy: + now = " " + _("To see them now, click the Unbury button below.") + else: + now = "" + line.append(_("""\ +Some related or buried cards were delayed until a later session.""")+now) + if self.haveCustomStudy and not self.col.decks.current()['dyn']: + line.append(_("""\ +To study outside of the normal schedule, click the Custom Study button below.""")) + return "

".join(line) + + def revDue(self): + "True if there are any rev cards due." + return self.col.db.scalar( + (f"select 1 from cards where did in %s and queue = {QUEUE_REV} " + "and due <= ? limit 1") % self._deckLimit(), + self.today) + + def newDue(self): + "True if there are any new cards due." + return self.col.db.scalar( + (f"select 1 from cards where did in %s and queue = {QUEUE_NEW_CRAM} " + "limit 1") % (self._deckLimit(),)) + + def haveBuriedSiblings(self): + sdids = ids2str(self.col.decks.active()) + cnt = self.col.db.scalar( + f"select 1 from cards where queue = {QUEUE_USER_BURIED} and did in %s limit 1" % (sdids)) + return not not cnt + + # Next time reports + ########################################################################## + + def nextIvlStr(self, card, ease, short=False): + "Return the next interval for CARD as a string." + ivl = self.nextIvl(card, ease) + if not ivl: + return _("(end)") + s = fmtTimeSpan(ivl, short=short) + if ivl < self.col.conf['collapseTime']: + s = "<"+s + return s + + # Suspending + ########################################################################## + + def buryNote(self, nid): + "Bury all cards for note until next session." + cids = self.col.db.list( + "select id from cards where nid = ? and queue >= 0", nid) + self.buryCards(cids) + + # Sibling spacing + ########################################################################## + + def _burySiblings(self, card): + toBury = [] + nconf = self._newConf(card) + buryNew = nconf.get("bury", True) + rconf = self._revConf(card) + buryRev = rconf.get("bury", True) + # loop through and remove from queues + for cid,queue in self.col.db.execute(f""" +select id, queue from cards where nid=? and id!=? +and (queue={QUEUE_NEW_CRAM} or (queue={QUEUE_REV} and due<=?))""", + card.nid, card.id, self.today): + if queue == QUEUE_REV: + if buryRev: + toBury.append(cid) + # if bury disabled, we still discard to give same-day spacing + try: + self._revQueue.remove(cid) + except ValueError: + pass + else:#Queue new Cram + # if bury disabled, we still discard to give same-day spacing + if buryNew: + toBury.append(cid) + try: + self._newQueue.remove(cid) + except ValueError: + pass + # burying is done by the concrete class + return toBury + + # Resetting + ########################################################################## + + def forgetCards(self, ids): + "Put cards at the end of the new queue." + self.remFromDyn(ids) + self.col.db.execute( + (f"update cards set type={CARD_NEW},queue={QUEUE_NEW_CRAM},ivl=0,due=0,odue=0,factor=?" + " where id in ")+ids2str(ids), STARTING_FACTOR) + pmax = self.col.db.scalar( + f"select max(due) from cards where type={CARD_NEW}") or 0 + # takes care of mod + usn + self.sortCards(ids, start=pmax+1) + self.col.log(ids) + + def reschedCards(self, ids, imin, imax): + "Put cards in review queue with a new interval in days (min, max)." + d = [] + t = self.today + mod = intTime() + for id in ids: + r = random.randint(imin, imax) + d.append(dict(id=id, due=r+t, ivl=max(1, r), mod=mod, + usn=self.col.usn(), fact=STARTING_FACTOR)) + self.remFromDyn(ids) + self.col.db.executemany(f""" +update cards set type={CARD_DUE},queue={QUEUE_REV},ivl=:ivl,due=:due,odue=0, +usn=:usn,mod=:mod,factor=:fact where id=:id""", + d) + self.col.log(ids) + + def resetCards(self, ids): + "Completely reset cards for export." + sids = ids2str(ids) + # we want to avoid resetting due number of existing new cards on export + nonNew = self.col.db.list( + f"select id from cards where id in %s and (queue != {QUEUE_NEW_CRAM} or type != {CARD_NEW})" + % sids) + # reset all cards + self.col.db.execute( + f"update cards set reps=0,lapses=0,odid=0,odue=0,queue={QUEUE_NEW_CRAM}" + " where id in %s" % sids + ) + # and forget any non-new cards, changing their due numbers + self.forgetCards(nonNew) + self.col.log(ids) + + # Repositioning new cards + ########################################################################## + + def sortCards(self, cids, start=1, step=1, shuffle=False, shift=False): + """Change the due of new cards in `cids`. + + Each card of the same note have the same due. The order of the + due is random if shuffle. Otherwise the order of the note `n` is + similar to the order of the first occurrence of a card of `n` in cids. + + Keyword arguments: + cids -- list of card ids to reorder (i.e. change due). Not new cards are ignored + start -- the first due to use + step -- the difference between to successive due of notes + shuffle -- whether to shuffle the note. By default, the order is similar to the created order + shift -- whether to change the due of all new cards whose due is greater than start (to ensure that the new due of cards in cids is not already used) + + """ + scids = ids2str(cids) + now = intTime() + nids = [] + nidsSet = set() + for id in cids: + nid = self.col.db.scalar("select nid from cards where id = ?", id) + if nid not in nidsSet: + nids.append(nid) + nidsSet.add(nid) + if not nids: + # no new cards + return + # determine nid ordering + due = {} + if shuffle: + random.shuffle(nids) + for c, nid in enumerate(nids): + due[nid] = start+c*step + # pylint: disable=undefined-loop-variable + high = start+c*step #Highest due which will be used + # shift? + if shift: + low = self.col.db.scalar( + f"select min(due) from cards where due >= ? and type = {CARD_NEW} " + "and id not in %s" % scids, + start) + if low is not None: + shiftby = high - low + 1 + self.col.db.execute(f""" +update cards set mod=?, usn=?, due=due+? where id not in %s +and due >= ? and queue = {QUEUE_NEW_CRAM}""" % scids, now, self.col.usn(), shiftby, low) + # reorder cards + d = [] + for id, nid in self.col.db.execute( + (f"select id, nid from cards where type = {CARD_NEW} and id in ")+scids): + d.append(dict(now=now, due=due[nid], usn=self.col.usn(), cid=id)) + self.col.db.executemany( + "update cards set due=:due,mod=:now,usn=:usn where id = :cid", d) + + def randomizeCards(self, did): + """Change the due value of new cards of deck `did`. The new due value + is the same for every card of a note (as long as they are in the same + deck.)""" + cids = self.col.db.list("select id from cards where did = ?", did) + self.sortCards(cids, shuffle=True) + + def orderCards(self, did): + """Change the due value of new cards of deck `did`. The new due value + is the same for every card of a note (as long as they are in the + same deck.) + + The note are now ordered according to the smallest id of their + cards. It generally means they are ordered according to date + creation. + """ + cids = self.col.db.list("select id from cards where did = ? order by id", did) + self.sortCards(cids) + + def resortConf(self, conf): + """When a deck configuration's order of new card is changed, apply + this change to each deck having the same deck configuration.""" + for did in self.col.decks.didsForConf(conf): + if conf['new']['order'] == NEW_CARDS_RANDOM: + self.randomizeCards(did) + else: + self.orderCards(did) + + # for post-import + def maybeRandomizeDeck(self, did=None): + if not did: + did = self.col.decks.selected() + conf = self.col.decks.confForDid(did) + # in order due? + if conf['new']['order'] == NEW_CARDS_RANDOM: + self.randomizeCards(did) diff --git a/anki/sched.py b/anki/sched.py index f4f78e3f94d..6436cd447c3 100644 --- a/anki/sched.py +++ b/anki/sched.py @@ -2,76 +2,11 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -import time -import random -import itertools -from operator import itemgetter -from heapq import * - -#from anki.cards import Card -from anki.utils import ids2str, intTime, fmtTimeSpan -from anki.lang import _ -from anki.consts import * -from anki.hooks import runHook - -# queue types: 0=new/cram, 1=lrn, 2=rev, 3=day lrn, -1=suspended, -2=buried -# revlog types: 0=lrn, 1=rev, 2=relrn, 3=cram -# positive revlog intervals are in days (rev), negative in seconds (lrn) - -class Scheduler: - """ - today -- difference between the last time scheduler is seen and creation of the collection. - dayCutoff -- The timestamp of when today end. - reportLimit -- the maximal number to show in main windows - lrnCount -- The number of cards in learning in selected decks - revCount -- number of cards to review today in selected decks - newCount -- number of new cards to see today in selected decks - _lrnDids, _revDids, _newDids -- a copy of the set of active decks where decks with no card to see today are removed. - _newQueue, _lrnQueue, _revQueue -- list of ids of cards in the queue new, lrn and rev. At most queue limit (i.e. 50) - queueLimit -- maximum number of cards to queue simultaneously. Always 50 unless changed by an addon. - _lrnDayQueue -- todo - newCardModulus -- each card in position 0 Modulo newCardModulus is a new card. Or it is 0 if new cards are not mixed with reviews. - -- _haveQueues -- whether the number of cards to see today for current decks have been set. - """ +from anki.bothSched import * + +class Scheduler(BothScheduler): name = "std" - haveCustomStudy = True _spreadRev = True - _burySiblingsOnAnswer = True - - def __init__(self, col): - self.col = col - self.queueLimit = 50 - self.reportLimit = 1000 - self.reps = 0 - self.today = None - self._haveQueues = False - self._updateCutoff() - - def getCard(self): - "Pop the next card from the queue. None if finished." - self._checkDay() - if not self._haveQueues: - self.reset() - card = self._getCard() - if card: - self.col.log(card) - if not self._burySiblingsOnAnswer: - self._burySiblings(card) - self.reps += 1 - card.startTimer() - return card - - def reset(self): - """ - Deal with the fact that it's potentially a new day. - Reset number of learning, review, new cards according to current decks - empty queues. Set haveQueues to true - """ - self._updateCutoff() - self._resetLrn() - self._resetRev() - self._resetNew() - self._haveQueues = True def answerCard(self, card, ease): """Change the number of card to see in the decks and its @@ -133,24 +68,6 @@ def counts(self, card=None): counts[idx] += 1 return tuple(counts) - def dueForecast(self, days=7): - "Return counts over next DAYS. Includes today." - daysd = dict(self.col.db.all(f""" -select due, count() from cards -where did in %s and queue = {QUEUE_REV} -and due between ? and ? -group by due -order by due""" % (self._deckLimit()), - self.today, - self.today+days-1)) - for d in range(days): - d = self.today+d - if d not in daysd: - daysd[d] = 0 - # return in sorted order - ret = [x[1] for x in sorted(daysd.items())] - return ret - def countIdx(self, card): """In which column the card in the queue should be counted. day_lrn is sent to lrn, otherwise its the identity""" @@ -159,6 +76,7 @@ def countIdx(self, card): return card.queue def answerButtons(self, card): + """The number of buttons to show in the reviewer for `card`""" if card.odue: # normal review in dyn deck? if card.odid and card.queue == QUEUE_REV: @@ -192,72 +110,6 @@ def unburyCardsForDeck(self): # Rev/lrn/time daily stats ########################################################################## - def _updateStats(self, card, type, cnt=1): - """Change the number of review/new/learn cards to see today in card's - deck, and in all of its ancestors. The number of card to see - is decreased by cnt. - - if type is time, it adds instead the time taken in this card - to this decks and all of its ancestors. - """ - key = type+"Today" - for g in ([self.col.decks.get(card.did)] + - self.col.decks.parents(card.did)): - # add - g[key][1] += cnt - self.col.decks.save(g) - - def extendLimits(self, new, rev): - """Decrease the limit of new/rev card to see today to this deck, its - ancestors and all of its descendant, by new/rev. - - This number is called from aqt.customstudy.CustomStudy.accept, with the number of card to study today. - """ - cur = self.col.decks.current() - parents = self.col.decks.parents(cur['id']) - children = [self.col.decks.get(did) for (name, did) in - self.col.decks.children(cur['id'])] - for g in [cur] + parents + children: - # add - g['newToday'][1] -= new - g['revToday'][1] -= rev - self.col.decks.save(g) - - def _walkingCount(self, limFn=None, cntFn=None): - """The sum of cntFn applied to each active deck. - - limFn -- function which associate to each deck obejct the maximum number of card to consider - cntFn -- function which, given a deck id and a limit, return a number of card at most equal to this limit.""" - tot = 0 - pcounts = {}# Associate from each id of a parent deck p, the maximal number of cards of deck p which can be seen, minus the card found for its descendant already considered - # for each of the active decks - nameMap = self.col.decks.nameMap() - for did in self.col.decks.active(): - # early alphas were setting the active ids as a str - did = int(did) - # get the individual deck's limit - lim = limFn(self.col.decks.get(did)) - if not lim: - continue - # check the parents - parents = self.col.decks.parents(did, nameMap) - for p in parents: - # add if missing - if p['id'] not in pcounts: - pcounts[p['id']] = limFn(p) - # take minimum of child and parent - lim = min(pcounts[p['id']], lim) - # see how many cards we actually have - cnt = cntFn(did, lim) - # if non-zero, decrement from parent counts - for p in parents: - pcounts[p['id']] -= cnt - # we may also be a parent - pcounts[did] = lim - cnt - # and add to running total - tot += cnt - return tot - # Deck list ########################################################################## @@ -311,22 +163,6 @@ def deckDueTree(self): nodes=self._groupChildren(nodes_) return nodes - def _groupChildren(self, grps): - """[subdeck name without parent parts, - did, rev, lrn, new (counting subdecks) - [recursively the same things for the children]] - - Keyword arguments: - grps -- [deckname, did, rev, lrn, new] - """ - # first, split the group names into components - for g in grps: - g[0] = g[0].split("::") - # and sort based on those components - grps.sort(key=itemgetter(0)) - # then run main function - return self._groupChildrenMain(grps) - def _groupChildrenMain(self, grps): """ [subdeck name without parent parts, @@ -404,107 +240,6 @@ def _getCard(self): # New cards ########################################################################## - def _resetNewCount(self): - """Set newCount to the counter of new cards for the active decks.""" - # Number of card in deck did, at most lim - def cntFn(did, lim): - ret = self.col.db.scalar(f""" -select count() from (select 1 from cards where -did = ? and queue = {QUEUE_NEW_CRAM} limit ?)""", did, lim) - return ret - self.newCount = self._walkingCount(self._deckNewLimitSingle, cntFn) - - def _resetNew(self): - """Set newCount, newDids, newCardModulus. Empty newQueue. """ - self._resetNewCount() - self._newDids = self.col.decks.active()[:] - self._newQueue = [] - self._updateNewCardRatio() - - def _fillNew(self): - """Whether there are new card in current decks to see today. - - If it is the case that the _newQueue is not empty - """ - if self._newQueue: - return True - if not self.newCount: - return False - while self._newDids: - did = self._newDids[0] - lim = min(self.queueLimit, self._deckNewLimit(did)) - if lim: - # fill the queue with the current did - self._newQueue = self.col.db.list(f""" - select id from cards where did = ? and queue = {QUEUE_NEW_CRAM} order by due,ord limit ?""" , did, lim) - if self._newQueue: - self._newQueue.reverse() - return True - # nothing left in the deck; move to next - self._newDids.pop(0) - if self.newCount: - # if we didn't get a card but the count is non-zero, - # we need to check again for any cards that were - # removed from the queue but not buried - self._resetNew() - return self._fillNew() - - def _getNewCard(self): - if self._fillNew(): - self.newCount -= 1 - return self.col.getCard(self._newQueue.pop()) - - def _updateNewCardRatio(self): - """set newCardModulus so that new cards are regularly mixed with review cards. At least 2. - Only if new and review should be mixed""" - if self.col.conf['newSpread'] == NEW_CARDS_DISTRIBUTE: - if self.newCount: - self.newCardModulus = ( - (self.newCount + self.revCount) // self.newCount) - # if there are cards to review, ensure modulo >= 2 - if self.revCount: - self.newCardModulus = max(2, self.newCardModulus) - return - self.newCardModulus = 0 - - def _timeForNewCard(self): - "True if it's time to display a new card when distributing." - if not self.newCount: - return False - if self.col.conf['newSpread'] == NEW_CARDS_LAST: - return False - elif self.col.conf['newSpread'] == NEW_CARDS_FIRST: - return True - elif self.newCardModulus: - return self.reps and self.reps % self.newCardModulus == 0 - - def _deckNewLimit(self, did, fn=None): - if not fn: - fn = self._deckNewLimitSingle - sel = self.col.decks.get(did) - lim = -1 - # for the deck and each of its parents - for g in [sel] + self.col.decks.parents(did): - rem = fn(g) - if lim == -1: - lim = rem - else: - lim = min(rem, lim) - return lim - - def _newForDeck(self, did, lim): - """The minimum between the number of new cards in this deck lim and self.reportLimit. - - keyword arguments: - did -- id of a deck - lim -- an upper bound for the returned number (in practice, the number of new cards to see by day for the deck's option) """ - if not lim: - return 0 - lim = min(lim, self.reportLimit) - return self.col.db.scalar(f""" -select count() from -(select 1 from cards where did = ? and queue = {QUEUE_NEW_CRAM} limit ?)""", did, lim) - def _deckNewLimitSingle(self, g): """Maximum number of new card to see today for deck g, not considering parent limit. @@ -520,13 +255,6 @@ def _deckNewLimitSingle(self, g): ret = max(0, c['new']['perDay'] - g['newToday'][1]) return ret - def totalNewForCurrentDeck(self): - return self.col.db.scalar( - f""" -select count() from cards where id in ( -select id from cards where did in %s and queue = {QUEUE_NEW_CRAM} limit ?)""" - % ids2str(self.col.decks.active()), self.reportLimit) - # Learning queues ########################################################################## @@ -544,26 +272,9 @@ def _resetLrnCount(self): and due <= ? limit %d""" % (self._deckLimit(), self.reportLimit), self.today) - def _resetLrn(self): - """Set lrnCount and _lrnDids. Empty _lrnQueue, lrnDayQueu.""" - self._resetLrnCount() - self._lrnQueue = [] - self._lrnDayQueue = [] - self._lrnDids = self.col.decks.active()[:] - # sub-day learning def _fillLrn(self): - if not self.lrnCount: - return False - if self._lrnQueue: - return True - self._lrnQueue = self.col.db.all(f""" -select due, id from cards where -did in %s and queue = {QUEUE_LRN} and due < :lim -limit %d""" % (self._deckLimit(), self.reportLimit), lim=self.dayCutoff) - # as it arrives sorted by did first, we need to sort it - self._lrnQueue.sort() - return self._lrnQueue + return super()._fillLrn(self.dayCutoff, f"({QUEUE_LRN})") def _getLrnCard(self, collapse=False): if self._fillLrn(): @@ -576,36 +287,6 @@ def _getLrnCard(self, collapse=False): self.lrnCount -= card.left // 1000 return card - # daily learning - def _fillLrnDay(self): - if not self.lrnCount: - return False - if self._lrnDayQueue: - return True - while self._lrnDids: - did = self._lrnDids[0] - # fill the queue with the current did - self._lrnDayQueue = self.col.db.list(f""" -select id from cards where -did = ? and queue = {QUEUE_DAY_LRN} and due <= ? limit ?""", - did, self.today, self.queueLimit) - if self._lrnDayQueue: - # order - r = random.Random() - r.seed(self.today) - r.shuffle(self._lrnDayQueue) - # is the current did empty? - if len(self._lrnDayQueue) < self.queueLimit: - self._lrnDids.pop(0) - return True - # nothing left in the deck; move to next - self._lrnDids.pop(0) - - def _getLrnDayCard(self): - if self._fillLrnDay(): - self.lrnCount -= 1 - return self.col.getCard(self._lrnDayQueue.pop()) - def _answerLrnCard(self, card, ease): # ease 1=no, 2=yes, 3=remove conf = self._lrnConf(card) @@ -668,23 +349,6 @@ def _answerLrnCard(self, card, ease): card.queue = QUEUE_DAY_LRN self._logLrn(card, ease, conf, leaving, type, lastLeft) - def _delayForGrade(self, conf, left): - """The number of second for the delay until the next time the card can - be reviewed. Assuming the number of left steps is left, - according to configuration conf - - """ - left = left % 1000 - try: - delay = conf['delays'][-left] - except IndexError: - if conf['delays']: - delay = conf['delays'][0] - else: - # user deleted final step; use dummy value - delay = 1 - return delay*60 - def _lrnConf(self, card): """ lapse configuration if the card was due(i.e. review card ?), otherwise new configuration. I don't get the point""" @@ -744,27 +408,6 @@ def _startingLeft(self, card): tod = self._leftToday(conf['delays'], tot) return tot + tod*1000 - def _leftToday(self, delays, left, now=None): - """The number of the last ```left``` steps that can be completed - before the day cutoff. Assuming the first step is done - ```now```. - - delays -- the list of delays - left -- the number of step to consider (at the end of the - list) - now -- the time at which the first step is done. - """ - if not now: - now = intTime() - delays = delays[-left:] - ok = 0 - for i in range(len(delays)): - now += delays[i]*60 - if now > self.dayCutoff: - break - ok = i - return ok+1 - def _graduatingIvl(self, card, conf, early, adj=True): """ The interval before the next review. @@ -796,36 +439,6 @@ def _graduatingIvl(self, card, conf, early, adj=True): else: return ideal - def _rescheduleNew(self, card, conf, early): - """Reschedule a new card that's graduated for the first time. - - Set its factor according to conf. - Set its interval. If it's lapsed in dynamic deck, use - _dynIvlBoost. - Otherwise, the interval is found in conf['ints'][1 if early - else 0]. - Change due date according to the interval. - Put initial factor. - """ - card.ivl = self._graduatingIvl(card, conf, early) - card.due = self.today+card.ivl - card.factor = conf['initialFactor'] - - def _logLrn(self, card, ease, conf, leaving, type, lastLeft): - lastIvl = -(self._delayForGrade(conf, lastLeft)) - ivl = card.ivl if leaving else -(self._delayForGrade(conf, card.left)) - def log(): - self.col.db.execute( - "insert into revlog values (?,?,?,?,?,?,?,?,?)", - int(time.time()*1000), card.id, self.col.usn(), ease, - ivl, lastIvl, card.factor, card.timeTaken(), type) - try: - log() - except: - # duplicate pk; retry in 10ms - time.sleep(0.01) - log() - def removeLrn(self, ids=None): "Remove cards from the learning queues." if ids: @@ -900,9 +513,7 @@ def cntFn(did, lim): self._deckRevLimitSingle, cntFn) def _resetRev(self): - """Set revCount, empty _revQueue, _revDids""" - self._resetRevCount() - self._revQueue = [] + super()._resetRev() self._revDids = self.col.decks.active()[:] def _fillRev(self): @@ -1106,39 +717,6 @@ def _nextRevIvl(self, card, ease): # interval capped? return min(interval, conf['maxIvl']) - def _fuzzedIvl(self, ivl): - """Return a randomly chosen number of day for the interval, - not far from ivl. - - See ../documentation/computing_intervals for a clearer version - of this documentation - """ - min, max = self._fuzzIvlRange(ivl) - return random.randint(min, max) - - def _fuzzIvlRange(self, ivl): - """Return an increasing pair of numbers. The new interval will be a - number randomly selected between the first and the second - element. - - See ../documentation/computing_intervals for a clearer version - of this documentation - - """ - if ivl < 2: - return [1, 1] - elif ivl == 2: - return [2, 3] - elif ivl < 7: - fuzz = int(ivl*0.25) - elif ivl < 30: - fuzz = max(2, int(ivl*0.15)) - else: - fuzz = max(4, int(ivl*0.05)) - # fuzz at least a day - fuzz = max(fuzz, 1) - return [ivl-fuzz, ivl+fuzz] - def _constrainedIvl(self, ivl, conf, prev): """A new interval. Ivl multiplie by the interval factor of this conf. Greater than prev. @@ -1146,11 +724,6 @@ def _constrainedIvl(self, ivl, conf, prev): new = ivl * conf.get('ivlFct', 1) return int(max(new, prev+1)) - def _daysLate(self, card): - "Number of days later than scheduled." - due = card.odue if card.odid else card.due - return max(0, self.today - due) - def _updateRevIvl(self, card, ease): """ Compute the next interval, fuzzy it, ensure ivl increase and is at most maxIvl. @@ -1177,20 +750,6 @@ def _adjRevIvl(self, card, idealIvl): # Dynamic deck handling ########################################################################## - def rebuildDyn(self, did=None): - "Rebuild a dynamic deck." - did = did or self.col.decks.selected() - deck = self.col.decks.get(did) - assert deck['dyn'] - # move any existing cards back first, then fill - self.emptyDyn(did) - ids = self._fillDyn(deck) - if not ids: - return - # and change to our new deck - self.col.decks.select(did) - return ids - def _fillDyn(self, deck): search, limit, order = deck['terms'][0] orderlimit = self._dynOrder(order, limit) @@ -1225,33 +784,6 @@ def emptyDyn(self, did, lim=None): due = odue, odue = 0, odid = 0, usn = ? where %s""" % (lim), self.col.usn()) - def remFromDyn(self, cids): - self.emptyDyn(None, "id in %s and odid" % ids2str(cids)) - - def _dynOrder(self, o, l): - if o == DYN_OLDEST: - t = "(select max(id) from revlog where cid=c.id)" - elif o == DYN_RANDOM: - t = "random()" - elif o == DYN_SMALLINT: - t = "ivl" - elif o == DYN_BIGINT: - t = "ivl desc" - elif o == DYN_LAPSES: - t = "lapses desc" - elif o == DYN_ADDED: - t = "n.id" - elif o == DYN_REVADDED: - t = "n.id desc" - elif o == DYN_DUE: - t = "c.due" - elif o == DYN_DUEPRIORITY: - t = f"(case when queue={QUEUE_REV} and due <= %d then (ivl / cast(%d-due+0.001 as real)) else 100000+due end)" % (self.today, self.today) - else: - # if we don't understand the term, default to due order - t = "c.due" - return t + " limit %d" % l - def _moveToDyn(self, did, ids): deck = self.col.decks.get(did) data = [] @@ -1323,74 +855,12 @@ def _checkLeech(self, card, conf): # Tools ########################################################################## - def _cardConf(self, card): - """The configuration of this card's deck. See decks.py - documentation to read more about them.""" - return self.col.decks.confForDid(card.did) - - def _newConf(self, card): - """The configuration for "new" of this card's deck.See decks.py - documentation to read more about them. - - """ - conf = self._cardConf(card) - # normal deck - if not card.odid: - return conf['new'] - # dynamic deck; override some attributes, use original deck for others - oconf = self.col.decks.confForDid(card.odid) - delays = conf['delays'] or oconf['new']['delays'] - return dict( - # original deck - ints=oconf['new']['ints'], - initialFactor=oconf['new']['initialFactor'], - bury=oconf['new'].get("bury", True), - # overrides - delays=delays, - separate=conf['separate'], - order=NEW_CARDS_DUE, - perDay=self.reportLimit - ) - - def _lapseConf(self, card): - """The configuration for "lapse" of this card's deck.See decks.py - documentation to read more about them. - - """ - conf = self._cardConf(card) - # normal deck - if not card.odid: - return conf['lapse'] - # dynamic deck; override some attributes, use original deck for others - oconf = self.col.decks.confForDid(card.odid) - delays = conf['delays'] or oconf['lapse']['delays'] - return dict( - # original deck - minInt=oconf['lapse']['minInt'], - leechFails=oconf['lapse']['leechFails'], - leechAction=oconf['lapse']['leechAction'], - mult=oconf['lapse']['mult'], - # overrides - delays=delays, - resched=conf['resched'], - ) - - def _revConf(self, card): - """The configuration for "review" of this card's deck.See decks.py - documentation to read more about them. - - """ - conf = self._cardConf(card) - # normal deck - if not card.odid: - return conf['rev'] - # dynamic deck - return self.col.decks.confForDid(card.odid)['rev'] - - def _deckLimit(self): - """The list of active decks, as comma separated parenthesized - string""" - return ids2str(self.col.decks.active()) + def _delays(self, conf, oconf, type): + if conf['delays']: + r = conf['delays'] + else: + r = oconf[type]['delays'] + return r def _resched(self, card): """Whether this review must be taken into account when this @@ -1432,91 +902,31 @@ def update(g): if unburied < self.today: self.unburyCards() - def _checkDay(self): - # check if the day has rolled over - if time.time() > self.dayCutoff: - self.reset() - # Deck finished state ########################################################################## - def finishedMsg(self): - return (""+_( - "Congratulations! You have finished this deck for now.")+ - "

" + self._nextDueMsg()) - - def _nextDueMsg(self): - line = [] - # the new line replacements are so we don't break translations - # in a point release - if self.revDue(): - line.append(_("""\ -Today's review limit has been reached, but there are still cards -waiting to be reviewed. For optimum memory, consider increasing -the daily limit in the options.""").replace("\n", " ")) - if self.newDue(): - line.append(_("""\ -There are more new cards available, but the daily limit has been -reached. You can increase the limit in the options, but please -bear in mind that the more new cards you introduce, the higher -your short-term review workload will become.""").replace("\n", " ")) - if self.haveBuried(): - if self.haveCustomStudy: - now = " " + _("To see them now, click the Unbury button below.") - else: - now = "" - line.append(_("""\ -Some related or buried cards were delayed until a later session.""")+now) - if self.haveCustomStudy and not self.col.decks.current()['dyn']: - line.append(_("""\ -To study outside of the normal schedule, click the Custom Study button below.""")) - return "

".join(line) - - def revDue(self): - "True if there are any rev cards due." - return self.col.db.scalar( - (f"select 1 from cards where did in %s and queue = {QUEUE_REV} " - "and due <= ? limit 1") % (self._deckLimit()), - self.today) - - def newDue(self): - "True if there are any new cards due." - return self.col.db.scalar( - (f"select 1 from cards where did in %s and queue = {QUEUE_NEW_CRAM} " - "limit 1") % (self._deckLimit(),)) - def haveBuried(self): - sdids = ids2str(self.col.decks.active()) - cnt = self.col.db.scalar( - f"select 1 from cards where queue = {QUEUE_USER_BURIED} and did in %s limit 1" % (sdids)) - return not not cnt + return self.haveBuriedSiblings() # Next time reports ########################################################################## - def nextIvlStr(self, card, ease, short=False): - "Return the next interval for CARD as a string." - ivl = self.nextIvl(card, ease) - if not ivl: - return _("(end)") - s = fmtTimeSpan(ivl, short=short) - if ivl < self.col.conf['collapseTime']: - s = "<"+s - return s - def nextIvl(self, card, ease): "Return the next interval for CARD, in seconds." if card.queue in (QUEUE_NEW_CRAM, QUEUE_LRN, QUEUE_DAY_LRN): - return self._nextLrnIvl(card, ease) + r = self._nextLrnIvl(card, ease) + return r elif ease == BUTTON_ONE: # lapsed conf = self._lapseConf(card) if conf['delays']: return conf['delays'][0]*60 - return self._nextLapseIvl(card, conf)*86400 + r = self._nextLapseIvl(card, conf)*86400 + return r else: # review - return self._nextRevIvl(card, ease)*86400 + r = self._nextRevIvl(card, ease)*86400 + return r # this isn't easily extracted from the learn code def _nextLrnIvl(self, card, ease): @@ -1569,42 +979,11 @@ def buryCards(self, cids): update cards set queue={QUEUE_USER_BURIED},mod=?,usn=? where id in """)+ids2str(cids), intTime(), self.col.usn()) - def buryNote(self, nid): - "Bury all cards for note until next session." - cids = self.col.db.list( - "select id from cards where nid = ? and queue >= 0", nid) - self.buryCards(cids) - # Sibling spacing ########################################################################## def _burySiblings(self, card): - toBury = [] - nconf = self._newConf(card) - buryNew = nconf.get("bury", True) - rconf = self._revConf(card) - buryRev = rconf.get("bury", True) - # loop through and remove from queues - for cid,queue in self.col.db.execute(f""" -select id, queue from cards where nid=? and id!=? -and (queue={QUEUE_NEW_CRAM} or (queue={QUEUE_REV} and due<=?))""", - card.nid, card.id, self.today): - if queue == QUEUE_REV: - if buryRev: - toBury.append(cid) - # if bury disabled, we still discard to give same-day spacing - try: - self._revQueue.remove(cid) - except ValueError: - pass - else:#Queue new Cram - # if bury disabled, we still discard to give same-day spacing - if buryNew: - toBury.append(cid) - try: - self._newQueue.remove(cid) - except ValueError: - pass + toBury = super()._burySiblings(card) # then bury if toBury: self.col.db.execute( @@ -1615,140 +994,5 @@ def _burySiblings(self, card): # Resetting ########################################################################## - def forgetCards(self, ids): - "Put cards at the end of the new queue." - self.remFromDyn(ids) - self.col.db.execute( - (f"update cards set type={CARD_NEW},queue={QUEUE_NEW_CRAM},ivl=0,due=0,odue=0,factor=?" - " where id in ")+ids2str(ids), STARTING_FACTOR) - pmax = self.col.db.scalar( - f"select max(due) from cards where type={CARD_NEW}") or 0 - # takes care of mod + usn - self.sortCards(ids, start=pmax+1) - self.col.log(ids) - - def reschedCards(self, ids, imin, imax): - "Put cards in review queue with a new interval in days (min, max)." - d = [] - t = self.today - mod = intTime() - for id in ids: - r = random.randint(imin, imax) - d.append(dict(id=id, due=r+t, ivl=max(1, r), mod=mod, - usn=self.col.usn(), fact=STARTING_FACTOR)) - self.remFromDyn(ids) - self.col.db.executemany(f""" -update cards set type={CARD_DUE},queue={QUEUE_REV},ivl=:ivl,due=:due,odue=0, -usn=:usn,mod=:mod,factor=:fact where id=:id""", - d) - self.col.log(ids) - - def resetCards(self, ids): - "Completely reset cards for export." - sids = ids2str(ids) - # we want to avoid resetting due number of existing new cards on export - nonNew = self.col.db.list( - f"select id from cards where id in %s and (queue != {QUEUE_NEW_CRAM} or type != {CARD_NEW})" - % (sids)) - # reset all cards - self.col.db.execute( - f"update cards set reps=0,lapses=0,odid=0,odue=0,queue={QUEUE_NEW_CRAM}" - " where id in %s" % (sids) - ) - # and forget any non-new cards, changing their due numbers - self.forgetCards(nonNew) - self.col.log(ids) - # Repositioning new cards ########################################################################## - - def sortCards(self, cids, start=1, step=1, shuffle=False, shift=False): - """Change the due of new cards in `cids`. - - Each card of the same note have the same due. The order of the - due is random if shuffle. Otherwise the order of the note `n` is - similar to the order of the first occurrence of a card of `n` in cids. - - Keyword arguments: - cids -- list of card ids to reorder (i.e. change due). Not new cards are ignored - start -- the first due to use - step -- the difference between to successive due of notes - shuffle -- whether to shuffle the note. By default, the order is similar to the created order - shift -- whether to change the due of all new cards whose due is greater than start (to ensure that the new due of cards in cids is not already used) - - """ - scids = ids2str(cids) - now = intTime() - nids = [] - nidsSet = set() - for id in cids: - nid = self.col.db.scalar("select nid from cards where id = ?", id) - if nid not in nidsSet: - nids.append(nid) - nidsSet.add(nid) - if not nids: - # no new cards - return - # determine nid ordering - due = {} - if shuffle: - random.shuffle(nids) - for c, nid in enumerate(nids): - due[nid] = start+c*step - # pylint: disable=undefined-loop-variable - high = start+c*step #Highest due which will be used - # shift? - if shift: - low = self.col.db.scalar( - f"select min(due) from cards where due >= ? and type = {CARD_NEW} " - "and id not in %s" % (scids), - start) - if low is not None: - shiftby = high - low + 1 - self.col.db.execute(f""" -update cards set mod=?, usn=?, due=due+? where id not in %s -and due >= ? and queue = {QUEUE_NEW_CRAM}""" % (scids), now, self.col.usn(), shiftby, low) - # reorder cards - d = [] - for id, nid in self.col.db.execute( - (f"select id, nid from cards where type = {CARD_NEW} and id in ")+scids): - d.append(dict(now=now, due=due[nid], usn=self.col.usn(), cid=id)) - self.col.db.executemany( - "update cards set due=:due,mod=:now,usn=:usn where id = :cid", d) - - def randomizeCards(self, did): - """Change the due value of new cards of deck `did`. The new due value - is the same for every card of a note (as long as they are in the same - deck.)""" - cids = self.col.db.list("select id from cards where did = ?", did) - self.sortCards(cids, shuffle=True) - - def orderCards(self, did): - """Change the due value of new cards of deck `did`. The new due value - is the same for every card of a note (as long as they are in the - same deck.) - - The note are now ordered according to the smallest id of their - cards. It generally means they are ordered according to date - creation. - """ - cids = self.col.db.list("select id from cards where did = ? order by id", did) - self.sortCards(cids) - - def resortConf(self, conf): - """When a deck configuration's order of new card is changed, apply - this change to each deck having the same deck configuration.""" - for did in self.col.decks.didsForConf(conf): - if conf['new']['order'] == NEW_CARDS_RANDOM: - self.randomizeCards(did) - else: - self.orderCards(did) - - # for post-import - def maybeRandomizeDeck(self, did=None): - if not did: - did = self.col.decks.selected() - conf = self.col.decks.confForDid(did) - # in order due? - if conf['new']['order'] == NEW_CARDS_RANDOM: - self.randomizeCards(did) diff --git a/anki/schedv2.py b/anki/schedv2.py index ea422d1383e..eed8bfd23b7 100644 --- a/anki/schedv2.py +++ b/anki/schedv2.py @@ -2,61 +2,15 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -import time -import random -import itertools -from operator import itemgetter -from heapq import * -import datetime - -from anki.utils import ids2str, intTime, fmtTimeSpan -from anki.lang import _ -from anki.consts import * -from anki.hooks import runHook - -# card types: 0=new, 1=lrn, 2=rev, 3=relrn -# queue types: 0=new, 1=(re)lrn, 2=rev, 3=day (re)lrn, -# 4=preview, -1=suspended, -2=sibling buried, -3=manually buried -# revlog types: 0=lrn, 1=rev, 2=relrn, 3=early review -# positive revlog intervals are in days (rev), negative in seconds (lrn) -# odue/odid store original due/did when cards moved to filtered deck - -class Scheduler: +from anki.bothSched import * + +class Scheduler(BothScheduler): name = "std2" - haveCustomStudy = True - _burySiblingsOnAnswer = True def __init__(self, col): - self.col = col - self.queueLimit = 50 - self.reportLimit = 1000 + super().__init__(col) self.dynReportLimit = 99999 - self.reps = 0 - self.today = None - self._haveQueues = False self._lrnCutoff = 0 - self._updateCutoff() - - def getCard(self): - "Pop the next card from the queue. None if finished." - self._checkDay() - if not self._haveQueues: - self.reset() - card = self._getCard() - if card: - self.col.log(card) - if not self._burySiblingsOnAnswer: - self._burySiblings(card) - self.reps += 1 - card.startTimer() - return card - - def reset(self): - self._updateCutoff() - self._resetLrn() - self._resetRev() - self._resetNew() - self._haveQueues = True def answerCard(self, card, ease): self.col.log() @@ -123,30 +77,13 @@ def counts(self, card=None): counts[idx] += 1 return tuple(counts) - def dueForecast(self, days=7): - "Return counts over next DAYS. Includes today." - daysd = dict(self.col.db.all(f""" -select due, count() from cards -where did in %s and queue = {QUEUE_REV} -and due between ? and ? -group by due -order by due""" % (self._deckLimit()), - self.today, - self.today+days-1)) - for d in range(days): - d = self.today+d - if d not in daysd: - daysd[d] = 0 - # return in sorted order - ret = [x[1] for x in sorted(daysd.items())] - return ret - def countIdx(self, card): if card.queue in (QUEUE_DAY_LRN,QUEUE_PREVIEW): return QUEUE_LRN return card.queue def answerButtons(self, card): + """The number of buttons to show in the reviewer for `card`""" conf = self._cardConf(card) if card.odid and not conf['resched']: return 2 @@ -155,56 +92,6 @@ def answerButtons(self, card): # Rev/lrn/time daily stats ########################################################################## - def _updateStats(self, card, type, cnt=1): - key = type+"Today" - for g in ([self.col.decks.get(card.did)] + - self.col.decks.parents(card.did)): - # add - g[key][1] += cnt - self.col.decks.save(g) - - def extendLimits(self, new, rev): - cur = self.col.decks.current() - parents = self.col.decks.parents(cur['id']) - children = [self.col.decks.get(did) for (name, did) in - self.col.decks.children(cur['id'])] - for g in [cur] + parents + children: - # add - g['newToday'][1] -= new - g['revToday'][1] -= rev - self.col.decks.save(g) - - def _walkingCount(self, limFn=None, cntFn=None): - tot = 0 - pcounts = {} - # for each of the active decks - nameMap = self.col.decks.nameMap() - for did in self.col.decks.active(): - # early alphas were setting the active ids as a str - did = int(did) - # get the individual deck's limit - lim = limFn(self.col.decks.get(did)) - if not lim: - continue - # check the parents - parents = self.col.decks.parents(did, nameMap) - for p in parents: - # add if missing - if p['id'] not in pcounts: - pcounts[p['id']] = limFn(p) - # take minimum of child and parent - lim = min(pcounts[p['id']], lim) - # see how many cards we actually have - cnt = cntFn(did, lim) - # if non-zero, decrement from parent counts - for p in parents: - pcounts[p['id']] -= cnt - # we may also be a parent - pcounts[did] = lim - cnt - # and add to running total - tot += cnt - return tot - # Deck list ########################################################################## @@ -248,15 +135,6 @@ def parent(name): def deckDueTree(self): return self._groupChildren(self.deckDueList()) - def _groupChildren(self, grps): - # first, split the group names into components - for g in grps: - g[0] = g[0].split("::") - # and sort based on those components - grps.sort(key=itemgetter(0)) - # then run main function - return self._groupChildrenMain(grps) - def _groupChildrenMain(self, grps): tree = [] # group and recurse @@ -336,92 +214,6 @@ def _getCard(self): # New cards ########################################################################## - def _resetNewCount(self): - cntFn = lambda did, lim: self.col.db.scalar(f""" -select count() from (select 1 from cards where -did = ? and queue = {QUEUE_NEW_CRAM} limit ?)""", did, lim) - self.newCount = self._walkingCount(self._deckNewLimitSingle, cntFn) - - def _resetNew(self): - self._resetNewCount() - self._newDids = self.col.decks.active()[:] - self._newQueue = [] - self._updateNewCardRatio() - - def _fillNew(self): - if self._newQueue: - return True - if not self.newCount: - return False - while self._newDids: - did = self._newDids[0] - lim = min(self.queueLimit, self._deckNewLimit(did)) - if lim: - # fill the queue with the current did - self._newQueue = self.col.db.list(f""" - select id from cards where did = ? and queue = {QUEUE_NEW_CRAM} order by due,ord limit ?""", did, lim) - if self._newQueue: - self._newQueue.reverse() - return True - # nothing left in the deck; move to next - self._newDids.pop(0) - if self.newCount: - # if we didn't get a card but the count is non-zero, - # we need to check again for any cards that were - # removed from the queue but not buried - self._resetNew() - return self._fillNew() - - def _getNewCard(self): - if self._fillNew(): - self.newCount -= 1 - return self.col.getCard(self._newQueue.pop()) - - def _updateNewCardRatio(self): - if self.col.conf['newSpread'] == NEW_CARDS_DISTRIBUTE: - if self.newCount: - self.newCardModulus = ( - (self.newCount + self.revCount) // self.newCount) - # if there are cards to review, ensure modulo >= 2 - if self.revCount: - self.newCardModulus = max(2, self.newCardModulus) - return - self.newCardModulus = 0 - - def _timeForNewCard(self): - "True if it's time to display a new card when distributing." - if not self.newCount: - return False - if self.col.conf['newSpread'] == NEW_CARDS_LAST: - return False - elif self.col.conf['newSpread'] == NEW_CARDS_FIRST: - return True - elif self.newCardModulus: - return self.reps and self.reps % self.newCardModulus == 0 - - def _deckNewLimit(self, did, fn=None): - if not fn: - fn = self._deckNewLimitSingle - sel = self.col.decks.get(did) - lim = -1 - # for the deck and each of its parents - for g in [sel] + self.col.decks.parents(did): - rem = fn(g) - if lim == -1: - lim = rem - else: - lim = min(rem, lim) - return lim - - def _newForDeck(self, did, lim): - "New count for a single deck." - if not lim: - return 0 - lim = min(lim, self.reportLimit) - return self.col.db.scalar(f""" -select count() from -(select 1 from cards where did = ? and queue = {QUEUE_NEW_CRAM} limit ?)""", did, lim) - def _deckNewLimitSingle(self, g): "Limit for deck without parent limits." if g['dyn']: @@ -429,13 +221,6 @@ def _deckNewLimitSingle(self, g): c = self.col.decks.confForDid(g['id']) return max(0, c['new']['perDay'] - g['newToday'][1]) - def totalNewForCurrentDeck(self): - return self.col.db.scalar( - f""" -select count() from cards where id in ( -select id from cards where did in %s and queue = {QUEUE_NEW_CRAM} limit ?)""" - % ids2str(self.col.decks.active()), self.reportLimit) - # Learning queues ########################################################################## @@ -470,25 +255,11 @@ def _resetLrnCount(self): def _resetLrn(self): self._updateLrnCutoff(force=True) - self._resetLrnCount() - self._lrnQueue = [] - self._lrnDayQueue = [] - self._lrnDids = self.col.decks.active()[:] + return super()._resetLrn() # sub-day learning def _fillLrn(self): - if not self.lrnCount: - return False - if self._lrnQueue: - return True - cutoff = intTime() + self.col.conf['collapseTime'] - self._lrnQueue = self.col.db.all(f""" -select due, id from cards where -did in %s and queue in ({QUEUE_LRN},{QUEUE_PREVIEW}) and due < :lim -limit %d""" % (self._deckLimit(), self.reportLimit), lim=cutoff) - # as it arrives sorted by did first, we need to sort it - self._lrnQueue.sort() - return self._lrnQueue + return super()._fillLrn(intTime() + self.col.conf['collapseTime'], f"({QUEUE_LRN},{QUEUE_PREVIEW})") def _getLrnCard(self, collapse=False): self._maybeResetLrn(force=collapse and self.lrnCount == 0) @@ -502,35 +273,6 @@ def _getLrnCard(self, collapse=False): self.lrnCount -= 1 return card - # daily learning - def _fillLrnDay(self): - if not self.lrnCount: - return False - if self._lrnDayQueue: - return True - while self._lrnDids: - did = self._lrnDids[0] - # fill the queue with the current did - self._lrnDayQueue = self.col.db.list((f""" -select id from cards where -did = ? and queue = {QUEUE_DAY_LRN} and due <= ? limit ?"""), - did, self.today, self.queueLimit) - if self._lrnDayQueue: - # order - r = random.Random() - r.seed(self.today) - r.shuffle(self._lrnDayQueue) - # is the current did empty? - if len(self._lrnDayQueue) < self.queueLimit: - self._lrnDids.pop(0) - return True - # nothing left in the deck; move to next - self._lrnDids.pop(0) - - def _getLrnDayCard(self): - if self._fillLrnDay(): - self.lrnCount -= 1 - return self.col.getCard(self._lrnDayQueue.pop()) def _answerLrnCard(self, card, ease): conf = self._lrnConf(card) @@ -617,18 +359,6 @@ def _rescheduleLrnCard(self, card, conf, delay=None): card.queue = QUEUE_DAY_LRN return delay - def _delayForGrade(self, conf, left): - left = left % 1000 - try: - delay = conf['delays'][-left] - except IndexError: - if conf['delays']: - delay = conf['delays'][0] - else: - # user deleted final step; use dummy value - delay = 1 - return delay*60 - def _delayForRepeatingGrade(self, conf, left): # halfway between last and next delay1 = self._delayForGrade(conf, left) @@ -671,19 +401,6 @@ def _startingLeft(self, card): tod = self._leftToday(conf['delays'], tot) return tot + tod*1000 - def _leftToday(self, delays, left, now=None): - "The number of steps that can be completed by the day cutoff." - if not now: - now = intTime() - delays = delays[-left:] - ok = 0 - for i in range(len(delays)): - now += delays[i]*60 - if now > self.dayCutoff: - break - ok = i - return ok+1 - def _graduatingIvl(self, card, conf, early, fuzz=True): if card.type in (CARD_DUE, CARD_FILTERED): return card.ivl @@ -698,28 +415,10 @@ def _graduatingIvl(self, card, conf, early, fuzz=True): return ideal def _rescheduleNew(self, card, conf, early): - "Reschedule a new card that's graduated for the first time." - card.ivl = self._graduatingIvl(card, conf, early) - card.due = self.today+card.ivl - card.factor = conf['initialFactor'] + super()._rescheduleNew(card, conf, early) card.type = CARD_DUE card.queue = QUEUE_REV - def _logLrn(self, card, ease, conf, leaving, type, lastLeft): - lastIvl = -(self._delayForGrade(conf, lastLeft)) - ivl = card.ivl if leaving else -(self._delayForGrade(conf, card.left)) - def log(): - self.col.db.execute( - "insert into revlog values (?,?,?,?,?,?,?,?,?)", - int(time.time()*1000), card.id, self.col.usn(), ease, - ivl, lastIvl, card.factor, card.timeTaken(), type) - try: - log() - except: - # duplicate pk; retry in 10ms - time.sleep(0.01) - log() - def _lrnForDeck(self, did): cnt = self.col.db.scalar( f""" @@ -779,10 +478,6 @@ def _resetRevCount(self): ids2str(self.col.decks.active()), self.today) - def _resetRev(self): - self._resetRevCount() - self._revQueue = [] - def _fillRev(self): if self._revQueue: return True @@ -816,18 +511,6 @@ def _fillRev(self): self._resetRev() return self._fillRev() - def _getRevCard(self): - if self._fillRev(): - self.revCount -= 1 - return self.col.getCard(self._revQueue.pop()) - - def totalRevForCurrentDeck(self): - return self.col.db.scalar( - f""" -select count() from cards where id in ( -select id from cards where did in %s and queue = {QUEUE_REV} and due <= ? limit ?)""" - % ids2str(self.col.decks.active()), self.today, self.reportLimit) - # Answering a review card ########################################################################## @@ -923,25 +606,6 @@ def _nextRevIvl(self, card, ease, fuzz): (card.ivl + delay) * fct * conf['ease4'], conf, ivl3, fuzz) return ivl4 - def _fuzzedIvl(self, ivl): - min, max = self._fuzzIvlRange(ivl) - return random.randint(min, max) - - def _fuzzIvlRange(self, ivl): - if ivl < 2: - return [1, 1] - elif ivl == 2: - return [2, 3] - elif ivl < 7: - fuzz = int(ivl*0.25) - elif ivl < 30: - fuzz = max(2, int(ivl*0.15)) - else: - fuzz = max(4, int(ivl*0.05)) - # fuzz at least a day - fuzz = max(fuzz, 1) - return [ivl-fuzz, ivl+fuzz] - def _constrainedIvl(self, ivl, conf, prev, fuzz): ivl = int(ivl * conf.get('ivlFct', 1)) if fuzz: @@ -950,11 +614,6 @@ def _constrainedIvl(self, ivl, conf, prev, fuzz): ivl = min(ivl, conf['maxIvl']) return int(ivl) - def _daysLate(self, card): - "Number of days later than scheduled." - due = card.odue if card.odid else card.due - return max(0, self.today - due) - def _updateRevIvl(self, card, ease): card.ivl = self._nextRevIvl(card, ease, fuzz=True) @@ -1000,20 +659,6 @@ def _earlyReviewIvl(self, card, ease): # Dynamic deck handling ########################################################################## - def rebuildDyn(self, did=None): - "Rebuild a dynamic deck." - did = did or self.col.decks.selected() - deck = self.col.decks.get(did) - assert deck['dyn'] - # move any existing cards back first, then fill - self.emptyDyn(did) - cnt = self._fillDyn(deck) - if not cnt: - return - # and change to our new deck - self.col.decks.select(did) - return cnt - def _fillDyn(self, deck): start = -100000 total = 0 @@ -1043,33 +688,6 @@ def emptyDyn(self, did, lim=None): self._restoreQueueSnippet, lim), self.col.usn()) - def remFromDyn(self, cids): - self.emptyDyn(None, "id in %s and odid" % ids2str(cids)) - - def _dynOrder(self, o, l): - if o == DYN_OLDEST: - t = "(select max(id) from revlog where cid=c.id)" - elif o == DYN_RANDOM: - t = "random()" - elif o == DYN_SMALLINT: - t = "ivl" - elif o == DYN_BIGINT: - t = "ivl desc" - elif o == DYN_LAPSES: - t = "lapses desc" - elif o == DYN_ADDED: - t = "n.id" - elif o == DYN_REVADDED: - t = "n.id desc" - elif o == DYN_DUE: - t = "c.due" - elif o == DYN_DUEPRIORITY: - t = f"(case when queue={QUEUE_REV} and due <= %d then (ivl / cast(%d-due+0.001 as real)) else 100000+due end)" % (self.today, self.today) - else: - # if we don't understand the term, default to due order - t = "c.due" - return t + " limit %d" % l - def _moveToDyn(self, did, ids, start=-100000): deck = self.col.decks.get(did) data = [] @@ -1138,56 +756,8 @@ def _checkLeech(self, card, conf): # Tools ########################################################################## - def _cardConf(self, card): - return self.col.decks.confForDid(card.did) - - def _newConf(self, card): - conf = self._cardConf(card) - # normal deck - if not card.odid: - return conf['new'] - # dynamic deck; override some attributes, use original deck for others - oconf = self.col.decks.confForDid(card.odid) - return dict( - # original deck - ints=oconf['new']['ints'], - initialFactor=oconf['new']['initialFactor'], - bury=oconf['new'].get("bury", True), - delays=oconf['new']['delays'], - # overrides - separate=conf['separate'], - order=NEW_CARDS_DUE, - perDay=self.reportLimit - ) - - def _lapseConf(self, card): - conf = self._cardConf(card) - # normal deck - if not card.odid: - return conf['lapse'] - # dynamic deck; override some attributes, use original deck for others - oconf = self.col.decks.confForDid(card.odid) - return dict( - # original deck - minInt=oconf['lapse']['minInt'], - leechFails=oconf['lapse']['leechFails'], - leechAction=oconf['lapse']['leechAction'], - mult=oconf['lapse']['mult'], - delays=oconf['lapse']['delays'], - # overrides - resched=conf['resched'], - ) - - def _revConf(self, card): - conf = self._cardConf(card) - # normal deck - if not card.odid: - return conf['rev'] - # dynamic deck - return self.col.decks.confForDid(card.odid)['rev'] - - def _deckLimit(self): - return ids2str(self.col.decks.active()) + def _delays(self, conf, oconf, type): + return oconf[type]['delays'] def _previewingCard(self, card): conf = self._cardConf(card) @@ -1222,11 +792,6 @@ def update(g): self.unburyCards() self.col.conf['lastUnburied'] = self.today - def _checkDay(self): - # check if the day has rolled over - if time.time() > self.dayCutoff: - self.reset() - def _dayCutoff(self): rolloverTime = self.col.conf.get("rollover", 4) if rolloverTime < 0: @@ -1248,57 +813,6 @@ def _daysSinceCreation(self): # Deck finished state ########################################################################## - def finishedMsg(self): - return (""+_( - "Congratulations! You have finished this deck for now.")+ - "

" + self._nextDueMsg()) - - def _nextDueMsg(self): - line = [] - # the new line replacements are so we don't break translations - # in a point release - if self.revDue(): - line.append(_("""\ -Today's review limit has been reached, but there are still cards -waiting to be reviewed. For optimum memory, consider increasing -the daily limit in the options.""").replace("\n", " ")) - if self.newDue(): - line.append(_("""\ -There are more new cards available, but the daily limit has been -reached. You can increase the limit in the options, but please -bear in mind that the more new cards you introduce, the higher -your short-term review workload will become.""").replace("\n", " ")) - if self.haveBuried(): - if self.haveCustomStudy: - now = " " + _("To see them now, click the Unbury button below.") - else: - now = "" - line.append(_("""\ -Some related or buried cards were delayed until a later session.""")+now) - if self.haveCustomStudy and not self.col.decks.current()['dyn']: - line.append(_("""\ -To study outside of the normal schedule, click the Custom Study button below.""")) - return "

".join(line) - - def revDue(self): - "True if there are any rev cards due." - return self.col.db.scalar( - (f"select 1 from cards where did in %s and queue = {QUEUE_REV} " - "and due <= ? limit 1") % self._deckLimit(), - self.today) - - def newDue(self): - "True if there are any new cards due." - return self.col.db.scalar( - (f"select 1 from cards where did in %s and queue = {QUEUE_NEW_CRAM} " - "limit 1") % self._deckLimit()) - - def haveBuriedSiblings(self): - sdids = ids2str(self.col.decks.active()) - cnt = self.col.db.scalar( - f"select 1 from cards where queue = {QUEUE_USER_BURIED} and did in %s limit 1" % sdids) - return not not cnt - def haveManuallyBuried(self): sdids = ids2str(self.col.decks.active()) cnt = self.col.db.scalar( @@ -1311,16 +825,6 @@ def haveBuried(self): # Next time reports ########################################################################## - def nextIvlStr(self, card, ease, short=False): - "Return the next interval for CARD as a string." - ivl = self.nextIvl(card, ease) - if not ivl: - return _("(end)") - s = fmtTimeSpan(ivl, short=short) - if ivl < self.col.conf['collapseTime']: - s = "<"+s - return s - def nextIvl(self, card, ease): "Return the next interval for CARD, in seconds." # preview mode? @@ -1328,23 +832,26 @@ def nextIvl(self, card, ease): if ease == BUTTON_ONE: return self._previewDelay(card) return 0 - # (re)learning? if card.queue in (QUEUE_NEW_CRAM, QUEUE_LRN, QUEUE_DAY_LRN): - return self._nextLrnIvl(card, ease) + r = self._nextLrnIvl(card, ease) + return r elif ease == BUTTON_ONE: # lapse conf = self._lapseConf(card) if conf['delays']: return conf['delays'][0]*60 - return self._lapseIvl(card, conf)*86400 + r = self._lapseIvl(card, conf)*86400 + return r else: # review early = card.odid and (card.odue > self.today) if early: - return self._earlyReviewIvl(card, ease)*86400 + r = self._earlyReviewIvl(card, ease)*86400 + return r else: - return self._nextRevIvl(card, ease, fuzz=False)*86400 + r = self._nextRevIvl(card, ease, fuzz=False)*86400 + return r # this isn't easily extracted from the learn code def _nextLrnIvl(self, card, ease): @@ -1400,12 +907,6 @@ def buryCards(self, cids, manual=True): update cards set queue=?,mod=?,usn=? where id in """+ids2str(cids), queue, intTime(), self.col.usn()) - def buryNote(self, nid): - "Bury all cards for note until next session." - cids = self.col.db.list( - "select id from cards where nid = ? and queue >= 0", nid) - self.buryCards(cids) - def unburyCards(self): "Unbury all buried cards in all decks." self.col.log( @@ -1435,150 +936,11 @@ def unburyCardsForDeck(self, type="all"): ########################################################################## def _burySiblings(self, card): - toBury = [] - nconf = self._newConf(card) - buryNew = nconf.get("bury", True) - rconf = self._revConf(card) - buryRev = rconf.get("bury", True) - # loop through and remove from queues - for cid,queue in self.col.db.execute(f""" -select id, queue from cards where nid=? and id!=? -and (queue={QUEUE_NEW_CRAM} or (queue={QUEUE_REV} and due<=?))""", - card.nid, card.id, self.today): - if queue == QUEUE_REV: - if buryRev: - toBury.append(cid) - # if bury disabled, we still discard to give same-day spacing - try: - self._revQueue.remove(cid) - except ValueError: - pass - else: - # if bury disabled, we still discard to give same-day spacing - if buryNew: - toBury.append(cid) - try: - self._newQueue.remove(cid) - except ValueError: - pass + toBury = super()._burySiblings(card) # then bury if toBury: self.buryCards(toBury, manual=False) - # Resetting - ########################################################################## - - def forgetCards(self, ids): - "Put cards at the end of the new queue." - self.remFromDyn(ids) - self.col.db.execute( - (f"update cards set type={CARD_NEW},queue={QUEUE_NEW_CRAM},ivl=0,due=0,odue=0,factor=?" - " where id in ")+ids2str(ids), STARTING_FACTOR) - pmax = self.col.db.scalar( - f"select max(due) from cards where type={CARD_NEW}") or 0 - # takes care of mod + usn - self.sortCards(ids, start=pmax+1) - self.col.log(ids) - - def reschedCards(self, ids, imin, imax): - "Put cards in review queue with a new interval in days (min, max)." - d = [] - t = self.today - mod = intTime() - for id in ids: - r = random.randint(imin, imax) - d.append(dict(id=id, due=r+t, ivl=max(1, r), mod=mod, - usn=self.col.usn(), fact=STARTING_FACTOR)) - self.remFromDyn(ids) - self.col.db.executemany(f""" -update cards set type={CARD_DUE},queue={QUEUE_REV},ivl=:ivl,due=:due,odue=0, -usn=:usn,mod=:mod,factor=:fact where id=:id""", - d) - self.col.log(ids) - - def resetCards(self, ids): - "Completely reset cards for export." - sids = ids2str(ids) - # we want to avoid resetting due number of existing new cards on export - nonNew = self.col.db.list( - f"select id from cards where id in %s and (queue != {QUEUE_NEW_CRAM} or type != {CARD_NEW})" - % sids) - # reset all cards - self.col.db.execute( - f"update cards set reps=0,lapses=0,odid=0,odue=0,queue={QUEUE_NEW_CRAM}" - " where id in %s" % sids - ) - # and forget any non-new cards, changing their due numbers - self.forgetCards(nonNew) - self.col.log(ids) - - # Repositioning new cards - ########################################################################## - - def sortCards(self, cids, start=1, step=1, shuffle=False, shift=False): - scids = ids2str(cids) - now = intTime() - nids = [] - nidsSet = set() - for id in cids: - nid = self.col.db.scalar("select nid from cards where id = ?", id) - if nid not in nidsSet: - nids.append(nid) - nidsSet.add(nid) - if not nids: - # no new cards - return - # determine nid ordering - due = {} - if shuffle: - random.shuffle(nids) - for c, nid in enumerate(nids): - due[nid] = start+c*step - # pylint: disable=undefined-loop-variable - high = start+c*step - # shift? - if shift: - low = self.col.db.scalar( - f"select min(due) from cards where due >= ? and type = {CARD_NEW} " - "and id not in %s" % scids, - start) - if low is not None: - shiftby = high - low + 1 - self.col.db.execute(f""" -update cards set mod=?, usn=?, due=due+? where id not in %s -and due >= ? and queue = {QUEUE_NEW_CRAM}""" % scids, now, self.col.usn(), shiftby, low) - # reorder cards - d = [] - for id, nid in self.col.db.execute( - (f"select id, nid from cards where type = {CARD_NEW} and id in ")+scids): - d.append(dict(now=now, due=due[nid], usn=self.col.usn(), cid=id)) - self.col.db.executemany( - "update cards set due=:due,mod=:now,usn=:usn where id = :cid", d) - - def randomizeCards(self, did): - cids = self.col.db.list("select id from cards where did = ?", did) - self.sortCards(cids, shuffle=True) - - def orderCards(self, did): - cids = self.col.db.list("select id from cards where did = ? order by id", did) - self.sortCards(cids) - - def resortConf(self, conf): - for did in self.col.decks.didsForConf(conf): - if conf['new']['order'] == NEW_CARDS_RANDOM: - self.randomizeCards(did) - else: - self.orderCards(did) - - # for post-import - def maybeRandomizeDeck(self, did=None): - if not did: - did = self.col.decks.selected() - conf = self.col.decks.confForDid(did) - # in order due? - if conf['new']['order'] == NEW_CARDS_RANDOM: - self.randomizeCards(did) - # Changing scheduler versions ########################################################################## diff --git a/tests/test_schedv1.py b/tests/test_schedv1.py index 669fa8a03d5..9fd727e5a4e 100644 --- a/tests/test_schedv1.py +++ b/tests/test_schedv1.py @@ -7,6 +7,7 @@ from tests.shared import getEmptyCol from anki.utils import intTime from anki.hooks import addHook +from anki.consts import * def test_clock(): @@ -574,7 +575,12 @@ def test_cram(): assert d.sched.counts() == (1,0,0) # grab it and check estimates c = d.sched.getCard() - assert d.sched.answerButtons(c) == 2 + assert c.type == CARD_DUE + lrnConf = d.sched._lrnConf(c) + assert d.sched.col.decks.confForDid(c.odid)['lapse']['delays'] == [10] + assert lrnConf['delays'] == [10] + ab = d.sched.answerButtons(c) + assert ab == 2 assert d.sched.nextIvl(c, 1) == 600 assert d.sched.nextIvl(c, 2) == 138*60*60*24 cram = d.decks.get(did) diff --git a/tests/test_schedv2.py b/tests/test_schedv2.py index 078cea8040e..19f2742bfb5 100644 --- a/tests/test_schedv2.py +++ b/tests/test_schedv2.py @@ -678,7 +678,9 @@ def test_filt_reviewing_early_normal(): # grab it and check estimates c = d.sched.getCard() assert d.sched.answerButtons(c) == 4 - assert d.sched.nextIvl(c, 1) == 600 + ivl = d.sched.nextIvl(c, 1) + assert d.sched._lapseConf(c)['delays'][0] == 10 + assert ivl == 600 assert d.sched.nextIvl(c, 2) == int(75*1.2)*86400 assert d.sched.nextIvl(c, 3) == int(75*2.5)*86400 assert d.sched.nextIvl(c, 4) == int(75*2.5*1.15)*86400 @@ -787,7 +789,9 @@ def test_preview(): # the other card should appear again c = d.sched.getCard() - assert c.id == orig.id + cid = c.id + origid = orig.id + assert cid == origid # emptying the filtered deck should restore card d.sched.emptyDyn(did) From 94070802d6c403362ca16e20b366637f798d0cc9 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Mon, 24 Jun 2019 19:05:25 +0200 Subject: [PATCH 27/68] ask to build while runanki --- runanki | 3 +++ 1 file changed, 3 insertions(+) diff --git a/runanki b/runanki index 7d212393716..06db1b029ca 100755 --- a/runanki +++ b/runanki @@ -1,4 +1,7 @@ #!/usr/bin/env python3 +import os +os.system("./tools/build_ui.sh") + import aqt aqt.run() From aa3c768e0daa69b92fe91897afad25c0a9f265d7 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Sat, 6 Jul 2019 03:21:11 +0200 Subject: [PATCH 28/68] Create add-on class --- anki/schedv2.py | 1 + aqt/addons.py | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/anki/schedv2.py b/anki/schedv2.py index eed8bfd23b7..d6e0bcee5eb 100644 --- a/anki/schedv2.py +++ b/anki/schedv2.py @@ -816,6 +816,7 @@ def _daysSinceCreation(self): def haveManuallyBuried(self): sdids = ids2str(self.col.decks.active()) cnt = self.col.db.scalar( + f"select 1 from cards where queue = {QUEUE_SCHED_BURIED} and did in %s limit 1" % sdids) return not not cnt diff --git a/aqt/addons.py b/aqt/addons.py index aea7c2222ef..70275bee3db 100644 --- a/aqt/addons.py +++ b/aqt/addons.py @@ -107,6 +107,8 @@ def addonsFolder(self, dir=None): def loadAddons(self): for dir in self.allAddons(): + if dir in incorporatedAddonsDict or (re.match(r"^\d+$", dir) and int(dir) in incorporatedAddonsDict): + continue meta = self.addonMeta(dir) if meta.get("disabled"): continue @@ -883,3 +885,23 @@ def accept(self): self.onClose() super().accept() + +## Add-ons incorporated in this fork. + +class Addon: + def __init__(self, name = None, id = None, mod = None, gitHash = None, gitRepo = None): + self.name = name + self.id = id + self.mod = mod + self.gitHash = gitHash + self.gitRepo = gitRepo + + def __hash__(self): + return self.id or hash(self.name) + +""" Set of characteristic of Add-ons incorporated here""" +incorporatedAddonsSet = { +} + +incorporatedAddonsDict = {**{addon.name: addon for addon in incorporatedAddonsSet if addon.name}, + **{addon.id: addon for addon in incorporatedAddonsSet if addon.id}} From efce534ed2a7ba2e5237b0192dd688fdae575ec6 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Sat, 6 Jul 2019 03:22:25 +0200 Subject: [PATCH 29/68] List of difference file --- difference.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 difference.md diff --git a/difference.md b/difference.md new file mode 100644 index 00000000000..eeaf6f0f9f3 --- /dev/null +++ b/difference.md @@ -0,0 +1,4 @@ +# Differences with anki. +This files list the difference between regular anki and this forked +version. It also lists the different options in the Preferences's extra page. + From 86ea04e126e9a86cb04b730f7c1631068f1e77e2 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Sat, 6 Jul 2019 03:22:32 +0200 Subject: [PATCH 30/68] Add-on folder --- addons/README.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 addons/README.md diff --git a/addons/README.md b/addons/README.md new file mode 100644 index 00000000000..d4d1bc125c2 --- /dev/null +++ b/addons/README.md @@ -0,0 +1,2 @@ +This directory contains a copy of each add-on, as they were when they +were added to anki From 96c4af905bee2c36dc3328b83e2988c152f57512 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Sat, 6 Jul 2019 03:21:31 +0200 Subject: [PATCH 31/68] Preferences: extra menu, and methods to update them --- aqt/preferences.py | 69 ++++++++++++++++++++++++++++++++++++----- designer/preferences.ui | 40 ++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 7 deletions(-) diff --git a/aqt/preferences.py b/aqt/preferences.py index 0808d888d72..32901cce08c 100644 --- a/aqt/preferences.py +++ b/aqt/preferences.py @@ -30,6 +30,7 @@ def __init__(self, mw): self.setupNetwork() self.setupBackup() self.setupOptions() + self.setupExtra() self.show() def accept(self): @@ -40,6 +41,7 @@ def accept(self): self.updateNetwork() self.updateBackup() self.updateOptions() + self.updateExtra() self.mw.pm.save() self.mw.reset() self.done(0) @@ -181,8 +183,7 @@ def _updateDayCutoffV2(self): def setupNetwork(self): self.form.syncOnProgramOpen.setChecked( self.prof['autoSync']) - self.form.syncMedia.setChecked( - self.prof['syncMedia']) + self.setupOneOptions('syncMedia') if not self.prof['syncKey']: self._hideAuth() else: @@ -203,7 +204,7 @@ def onSyncDeauth(self): def updateNetwork(self): self.prof['autoSync'] = self.form.syncOnProgramOpen.isChecked() - self.prof['syncMedia'] = self.form.syncMedia.isChecked() + self.updateOneOptions('syncMedia') if self.form.fullSync.isChecked(): self.mw.col.modSchema(check=False) self.mw.col.setMod() @@ -212,20 +213,74 @@ def updateNetwork(self): ###################################################################### def setupBackup(self): - self.form.numBackups.setValue(self.prof['numBackups']) + self.setupOneOptions("numBackups", 50, False) self.form.openBackupFolder.linkActivated.connect(self.onOpenBackup) def onOpenBackup(self): openFolder(self.mw.pm.backupFolder()) def updateBackup(self): - self.prof['numBackups'] = self.form.numBackups.value() + self.updateOneOptions("numBackups", 50, False) # Basic & Advanced Options ###################################################################### def setupOptions(self): - self.form.pastePNG.setChecked(self.prof.get("pastePNG", False)) + self.setupOneOptions("pastePNG") def updateOptions(self): - self.prof['pastePNG'] = self.form.pastePNG.isChecked() + self.updateOneOption("pastePNG") + + def setupOneOption(self, name, default=False, check=True, sync=True): + """Ensure that the preference manager default values for widget + `name` is set correctly. + + name -- name of the widget; it's also the name used in the configuration + default -- The value to take if the value is not in the profile/configuration + check -- whether it's a checkbox (otherwise, default value is 0) + sync -- whether the value should be synchronized. In this case + it is in configuration of collection. Otherwise it is in + profile's preference. + + """ + storeValue = self.mw.col.conf if sync else self.prof + widget = getattr(self.form, name) + if not check and default is False: + default = 0 + value = storeValue.get(name, default) + function = "setChecked" if check else "setValue" + getattr(widget, function)(value) + + def updateOneOption(self, name, default=False, check=True, sync=True): + """Save the value from the preference manager' widget + `name`. + + name -- name of the widget; it's also the name used in the + configuration + default -- not used here, only for having same parameters as setupOneOption + default -- The value to take if the value is not in the profile/configuration + check -- whether it's a checkbox (otherwise, default value is 0) + sync -- whether the value should be synchronized. In this case + it is in configuration of collection. Otherwise it is in + profile's preference. + + """ + storeValue = self.mw.col.conf if sync else self.prof + widget = getattr(self.form, name) + function = "isChecked" if check else "value" + value = getattr(widget, function)() + storeValue[name] = value + + extraOptions = [ + ] + + def setupExtra(self): + """Set in the GUI the preferences related to add-ons + forked.""" + for args in extraOptions: + self.setupOneOption(*args) + + def updateExtra(self): + """Check the preferences related to add-ons forked.""" + for args in extraOptions: + self.updateOneOption(*args) diff --git a/designer/preferences.ui b/designer/preferences.ui index 5f3ac480cea..0009a7bf405 100644 --- a/designer/preferences.ui +++ b/designer/preferences.ui @@ -465,6 +465,46 @@ + + + Extra + + + + + 0 + 0 + 401 + 471 + + + + + + + <html><head/><body><p><span style=" font-weight:600;">Extra</span><br/>Those options are not documented in anki's manual. They allow to configure the different add-ons incorporated in this special version of anki.</p></body></html> + + + true + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + From 37e5d807c2096c674551a4fdb91854c4294a7dec Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Thu, 27 Jun 2019 06:47:55 +0200 Subject: [PATCH 32/68] Added today --- addons/861864770/__init__.py | 9 ++++ addons/861864770/meta.json | 1 + addons/861864770/reviewer_browse_today.py | 54 +++++++++++++++++++++++ aqt/addcards.py | 16 ++++++- aqt/addons.py | 1 + difference.md | 3 ++ 6 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 addons/861864770/__init__.py create mode 100644 addons/861864770/meta.json create mode 100644 addons/861864770/reviewer_browse_today.py diff --git a/addons/861864770/__init__.py b/addons/861864770/__init__.py new file mode 100644 index 00000000000..f68d62e4cc0 --- /dev/null +++ b/addons/861864770/__init__.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# +# Entry point for the add-on into Anki +# Please do not edit this if you do not know what you are doing. +# +# Copyright: (c) 2017 Glutanimate +# License: GNU AGPLv3 + +from . import reviewer_browse_today \ No newline at end of file diff --git a/addons/861864770/meta.json b/addons/861864770/meta.json new file mode 100644 index 00000000000..d56beb69f48 --- /dev/null +++ b/addons/861864770/meta.json @@ -0,0 +1 @@ +{"name": "Open Added Today from Reviewer", "mod": 1561610680} \ No newline at end of file diff --git a/addons/861864770/reviewer_browse_today.py b/addons/861864770/reviewer_browse_today.py new file mode 100644 index 00000000000..e11fd352b6e --- /dev/null +++ b/addons/861864770/reviewer_browse_today.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- + +""" +Anki Add-on: Open 'Added Today' from Reviewer. + +Adds a menu item into the "History" menu of the "Add" notes dialog that +opens a Browser on the 'Added Today' view. + +Copyright: (c) Steve AW 2013 + (c) Glutanimate 2016-2017 +License: GNU AGPLv3 or later +""" + +from anki.lang import _ + +from aqt.qt import * +from aqt.addcards import AddCards +from anki.hooks import wrap, runHook, addHook +import aqt +from anki import version as anki_version +anki21 = anki_version.startswith("2.1.") + +def insert_open_browser_action(self, m): + """self -- AddCards object + m -- QMenu objet""" + m.addSeparator() + a = m.addAction("Open Browser on 'Added &Today'") + if anki21: + a.triggered.connect(lambda: show_browser_on_added_today(self)) + else: + a.connect(a, SIGNAL("triggered()"), + lambda self=self: show_browser_on_added_today(self)) + + +def show_browser_on_added_today(self): + """AddCards objects""" + browser = aqt.dialogs.open("Browser", self.mw) + browser.form.searchEdit.lineEdit().setText("added:1") + if anki21: + browser.onSearchActivated() + else: + browser.onSearch() + if u'noteCrt' in browser.model.activeCols: + col_index = browser.model.activeCols.index(u'noteCrt') + browser.onSortChanged(col_index, True) + browser.form.tableView.selectRow(0) + + +def mySetupButtons(self): + self.historyButton.setEnabled(True) + +AddCards.showBrowserOnAddedToday = show_browser_on_added_today +AddCards.setupButtons = wrap(AddCards.setupButtons, mySetupButtons, "after") +addHook("AddCards.onHistory", insert_open_browser_action) diff --git a/aqt/addcards.py b/aqt/addcards.py index 19c00d83fcb..1b74c6cc87b 100644 --- a/aqt/addcards.py +++ b/aqt/addcards.py @@ -75,7 +75,7 @@ def setupButtons(self): b.setShortcut(QKeySequence(sc)) b.setToolTip(_("Shortcut: %s") % shortcut(sc)) b.clicked.connect(self.onHistory) - b.setEnabled(False) + b.setEnabled(True) self.historyButton = b def setAndFocusNote(self, note): @@ -160,8 +160,22 @@ def onHistory(self): a = m.addAction(_("(Note deleted)")) a.setEnabled(False) runHook("AddCards.onHistory", self, m) + m.addSeparator() + a = m.addAction("Open Browser on 'Added &Today'") + a.triggered.connect(lambda: self.show_browser_on_added_today()) m.exec_(self.historyButton.mapToGlobal(QPoint(0,0))) + def show_browser_on_added_today(self): + browser = aqt.dialogs.open("Browser", self.mw) + browser.form.searchEdit.lineEdit().setText("added:1") + browser.onSearchActivated() + if u'noteCrt' in browser.model.activeCols: + col_index = browser.model.activeCols.index(u'noteCrt') + browser.onSortChanged(col_index, True) + browser.form.tableView.selectRow(0) + + + def editHistory(self, nid): browser = aqt.dialogs.open("Browser", self.mw) browser.form.searchEdit.lineEdit().setText("nid:%d" % nid) diff --git a/aqt/addons.py b/aqt/addons.py index 70275bee3db..e6b3320c32f 100644 --- a/aqt/addons.py +++ b/aqt/addons.py @@ -901,6 +901,7 @@ def __hash__(self): """ Set of characteristic of Add-ons incorporated here""" incorporatedAddonsSet = { + Addon("Open Added Today from Reviewer", 861864770, 1561610680, gitRepo = "https://github.com/glutanimate/anki-addons-misc"), #repo contains many add-ons. Thus hash seems useless. 47a218b21314f4ed7dd62397945c18fdfdfdff71 } incorporatedAddonsDict = {**{addon.name: addon for addon in incorporatedAddonsSet if addon.name}, diff --git a/difference.md b/difference.md index eeaf6f0f9f3..5aff7d8b129 100644 --- a/difference.md +++ b/difference.md @@ -2,3 +2,6 @@ This files list the difference between regular anki and this forked version. It also lists the different options in the Preferences's extra page. +## Added today (861864770) +From the add window page, you can see the list of cardes added today +in the browser. From 6f487b56c91735f2c8e549a1794f9d51fc429fb3 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Sat, 6 Jul 2019 04:34:29 +0200 Subject: [PATCH 33/68] Batch edit --- addons/291119185/CHANGELOG.md | 64 ++ addons/291119185/LICENSE.txt | 693 ++++++++++++++++++ addons/291119185/__init__.py | 41 ++ addons/291119185/_version.py | 36 + addons/291119185/data/patrons.py | 252 +++++++ addons/291119185/gui/__init__.py | 41 ++ addons/291119185/gui/resources/__init__.py | 43 ++ .../gui/resources/anki21/__init__.py | 20 + .../gui/resources/anki21/icons_rc.py | 113 +++ addons/291119185/main.py | 245 +++++++ addons/291119185/manifest.json | 12 + addons/291119185/meta.json | 1 + anki/find.py | 2 + aqt/addons.py | 1 + aqt/browser.py | 152 +++- aqt/editor.py | 2 +- aqt/preferences.py | 1 + aqt/utils.py | 32 +- designer/batchedit.ui | 156 ++++ designer/browser.ui | 11 +- designer/preferences.ui | 17 + difference.md | 6 + 22 files changed, 1902 insertions(+), 39 deletions(-) create mode 100644 addons/291119185/CHANGELOG.md create mode 100644 addons/291119185/LICENSE.txt create mode 100644 addons/291119185/__init__.py create mode 100644 addons/291119185/_version.py create mode 100644 addons/291119185/data/patrons.py create mode 100644 addons/291119185/gui/__init__.py create mode 100644 addons/291119185/gui/resources/__init__.py create mode 100644 addons/291119185/gui/resources/anki21/__init__.py create mode 100644 addons/291119185/gui/resources/anki21/icons_rc.py create mode 100644 addons/291119185/main.py create mode 100644 addons/291119185/manifest.json create mode 100644 addons/291119185/meta.json create mode 100644 designer/batchedit.ui diff --git a/addons/291119185/CHANGELOG.md b/addons/291119185/CHANGELOG.md new file mode 100644 index 00000000000..878654ad069 --- /dev/null +++ b/addons/291119185/CHANGELOG.md @@ -0,0 +1,64 @@ +# Changelog + +All notable changes to Batch Editing will be documented here. You can click on each release number to be directed to a detailed log of all code commits for that particular release. The download links will direct you to the GitHub release page, allowing you to manually install a release if you want. + +If you enjoy Batch Editing, please consider supporting my work on Patreon, or by buying me a cup of coffee :coffee:: + +

+      +

+ +:heart: My heartfelt thanks goes out to everyone who has supported this add-on through their tips, contributions, or any other means (you know who you are!). All of this would not have been possible without you. Thank you for being awesome! + +## [Unreleased] + +## [0.3.0] - 2019-06-02 + +### [Download](https://github.com/glutanimate/batch-editing/releases/tag/v0.3.0) + +### Fixed + +- 2.1: Fixed attaching images from the clipboard (#5, thanks to donfed for the report) +- 2.1: Fixed image icon (#5, thanks to reynie78 for the report) +- 2.1: Pre-emptive fix for anki.lang errors + +### Changed + +- Renamed add-on to "Batch Editing" +- Refactored add-on to improve stability and maintainability + +## 0.2.0 - 2017-08-23 + +### Added + +- Anki 2.1 compatibility + +## 0.1.3 - 2017-08-06 + +### Added + +- Ability to insert text as HTML + +## 0.1.2 - 2017-05-13 + +### Fixed + +- Only insert line-breaks when necessary + +## 0.1.1 - 2016-12-11 + +### Added + +- Support for adding text before existing content (thanks to @luminousspice for the idea) + +## 0.1.0 - 2016-12-08 + +### Added + +- Initial release of Batch Editing + +[Unreleased]: https://github.com/glutanimate/batch-editing/compare/v0.0.0...HEAD + +----- + +The format of this file is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). \ No newline at end of file diff --git a/addons/291119185/LICENSE.txt b/addons/291119185/LICENSE.txt new file mode 100644 index 00000000000..170d96748fe --- /dev/null +++ b/addons/291119185/LICENSE.txt @@ -0,0 +1,693 @@ +This Program is licensed under the GNU Affero General Public License +version 3 ("AGPL"), extended by a number of Additional Terms under +Section 7 of the AGPL. + +If not otherwise noted, this License applies to all files included +with this Program. Files subject to different licensing terms might +also ship with this Program, but will be clearly marked as such in +additional LICENSE files accompanying them. + +The AGPLv3 License and Additional Terms follow. + +============================================================================== + + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + +============================================================================== + + ADDITIONAL TERMS APPLICABLE TO THIS PROGRAM + UNDER GNU AGPL VERSION 3 SECTION 7 + +The following additional terms ("Additional Terms") supplement and modify the +GNU Affero General Public License, Version 3 ("AGPL") applicable to the present +Program. + +In addition to the terms and conditions of the AGPL, the present Program is +subject to the further restrictions below: + +1. Trademark and Publicity Rights. + + Except as expressly provided herein, no trademark or publicity rights are + granted. This license does NOT give you any right, title or interest in the + "Glutanimate" name or logo. + +2. Origin of the Program. + + The origin of the Program must not be misrepresented; you must not claim + that you wrote the original Program. Altered source versions must be plainly + marked as such, and must not be misrepresented as being the original + Program. + +3. Legal Notices and Author Attributions. + + You must reproduce faithfully all trademark, copyright and other proprietary + and legal notices on any copies of the Program or any other required author + attributions. Legal notices or author attributions displayed as part of the + user interface must be preserved as such. + +4. Use of Names of Licensors or Authors for Publicity Purposes. + + Outside of the aforementioned legal notices and author attributions, neither + the name of the copyright holder or its affiliates, any other party who + modifies and/or conveys the Program, nor the names of the Program's + sponsors/supporters/patrons may be used to endorse or promote products + derived from this software without specific prior written permission. + +5. Indemnification. + + IF YOU CONVEY A COVERED WORK AND AGREE WITH ANY RECIPIENT OF THAT COVERED + WORK THAT YOU WILL ASSUME ANY LIABILITY FOR THAT COVERED WORK, YOU HEREBY + AGREE TO INDEMNIFY, DEFEND AND HOLD HARMLESS THE OTHER LICENSORS AND AUTHORS + OF THAT COVERED WORK FOR ANY DAMAGES, DEMANDS, CLAIMS, LOSSES, CAUSES OF + ACTION, LAWSUITS, JUDGMENTS EXPENSES (INCLUDING WITHOUT LIMITATION + REASONABLE ATTORNEYS' FEES AND EXPENSES) OR ANY OTHER LIABLITY ARISING FROM, + RELATED TO OR IN CONNECTION WITH YOUR ASSUMPTIONS OF LIABILITY. + +6. Preservation of Licensing Terms. + + Any covered work conveyed by you must include this license text in its + entirety. + +------------------------------------------------------------------------------- + +If you have any questions regarding this license, about any other legal +details, or want to report an infringement of the aforementioned licensing +terms, please feel free to contact me at: diff --git a/addons/291119185/__init__.py b/addons/291119185/__init__.py new file mode 100644 index 00000000000..a00dcd2e722 --- /dev/null +++ b/addons/291119185/__init__.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- + +# Batch Editing Add-on for Anki +# +# Copyright (C) 2016-2019 Aristotelis P. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version, with the additions +# listed at the end of the license file that accompanied this program. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# NOTE: This program is subject to certain additional terms pursuant to +# Section 7 of the GNU Affero General Public License. You should have +# received a copy of these additional terms immediately following the +# terms and conditions of the GNU Affero General Public License that +# accompanied this program. +# +# If not, please request a copy through one of the means of contact +# listed here: . +# +# Any modifications to this file must keep this entire header intact. + +""" +Module-level entry point for the add-on into Anki 2.0/2.1 +""" + +from __future__ import (absolute_import, division, + print_function, unicode_literals) + +from ._version import __version__ # noqa: F401 + +from . import main # noqa: F401 diff --git a/addons/291119185/_version.py b/addons/291119185/_version.py new file mode 100644 index 00000000000..ea780177669 --- /dev/null +++ b/addons/291119185/_version.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- + +# Batch Editing Add-on for Anki +# +# Copyright (C) 2016-2019 Aristotelis P. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version, with the additions +# listed at the end of the license file that accompanied this program. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# NOTE: This program is subject to certain additional terms pursuant to +# Section 7 of the GNU Affero General Public License. You should have +# received a copy of these additional terms immediately following the +# terms and conditions of the GNU Affero General Public License that +# accompanied this program. +# +# If not, please request a copy through one of the means of contact +# listed here: . +# +# Any modifications to this file must keep this entire header intact. + +""" +Version information +""" + +__version__ = "0.3.0" diff --git a/addons/291119185/data/patrons.py b/addons/291119185/data/patrons.py new file mode 100644 index 00000000000..e923b7b77dd --- /dev/null +++ b/addons/291119185/data/patrons.py @@ -0,0 +1,252 @@ +# -*- coding: utf-8 -*- + +# List of Patreons supporting the development of this project +# +# +# +# Generated automatically by build scripts +# WARNING! All changes made in this file will be lost! + +from __future__ import unicode_literals + +MEMBERS_CREDITED = [ + "A", + "Aaron Bastian", + "Aaron Saunders", + "Aaron Yim", + "abed", + "Adam B Schweber", + "Aisha Lott", + "Albert Huang", + "Alexander Ryberg", + "Alistair Hussain", + "aman octain", + "Amin Najmabadi", + "Ana Patel", + "andrei gurau", + "Andrew Win", + "angelo adea", + "Anne-Astrid Agten", + "Annie", + "Anthony Hiran", + "aPaci", + "Arrowsmith", + "Artur", + "Ashton Huey", + "AT_Cyanide", + "Barbara Eve Wolanski", + "beets&beats", + "Blue", + "Bobak Seddighzadeh", + "Brandon Cunningham", + "Brodie", + "Bruno De Angeli", + "Cameron Walker", + "Carl", + "Catherine Rha", + "Chase", + "Chelonee", + "Chirag Bhatia", + "Chiryosha", + "Chris Wheate", + "Christopher Alfonso", + "Christopher Lee", + "Christy", + "Clemens", + "Clifton Lee", + "Clinton Sonier", + "Cássio Uítalo", + "Dan Robert", + "Daniel Higham", + "Daniel Sasson", + "Darrell Laddusaw Jr", + "David Dohan", + "David Liu", + "David Noël Hof", + "Devin Beecher", + "Diana Le", + "Doc Ambulance Driver", + "douly umer", + "Drannel Frodnetso", + "Edan Maor", + "Eric Bing", + "Evan Ware", + "Farhan Ahmad", + "Femi Osunnuga", + "Fernando Hernandez", + "Fernando Villasenor", + "Filip Davidovski", + "FloydianMuse", + "Frane", + "Gabriel Baião", + "gaksogeum", + "george", + "Georgi Perpeliev", + "Gui Ambros", + "H Brooks", + "Haian Mazyad", + "Hakim Khellaf", + "Hans Spewak", + "Harry Lang", + "Heathium", + "Henrik Giesel", + "Henrik Järvinen", + "Heron Leal Farias", + "Holly Crompton", + "Huascar Neves da Rocha", + "Hugo Marins", + "Isaiah Crum", + "Itai Efrat", + "Jack Elsey", + "Jaisil Muttakulath Joy", + "Jakub Kaminski", + "JChan", + "Jerry Tang", + "Jesse Tong", + "Jimjam", + "Jimmy Nguyen", + "Jimmy Stewart", + "Joaquin", + "Joe Shmoe", + "johana", + "John", + "John", + "John Hoff", + "John Meacock", + "John Smith", + "John Smith", + "John Vining", + "JooceMayn739", + "Jose Cepero", + "Junko Magnet", + "Jørgen Rahbek", + "K1ngJustic3", + "Ka Ngan Hui", + "Kamil Litwinowicz", + "Kari", + "Kayla N Marquez", + "Kerim Kaylan", + "Kevin George", + "Kirill Ochkan", + "kmcondit", + "Kowsikanth", + "Kyree", + "kılıç", + "Lane Conner", + "larsio", + "Levodopa", + "lintdusty", + "Logan Bingham", + "Luc Wastiaux", + "Lucas Teles De Carvalho Godoy Sampaio", + "Luis Felipe Coutinho", + "mak", + "Man Duong", + "Marilena Nicolaou", + "Mario", + "Mark", + "Marvin Carlisle", + "Mary Margaret Strange", + "Matt", + "Matthew McCullen", + "Max Loegler", + "Maximilian", + "Maximilien Brouchet", + "Michael Clark", + "Michael Cruz", + "Michael E Goldberg", + "Michael Song", + "Michail Pishchagin", + "MohnJadden7", + "Molly Schieber", + "monster909", + "murderedward", + "Nathan Little", + "Nicolas Curi", + "Nina Wade", + "Olaf Reibedanz", + "Oliver Grace", + "Oscar", + "Oscar Reyes", + "Over There", + "Paarth Kapadia", + "PapelMagico", + "Paul McManus", + "potka22", + "Prad", + "Rafael Asato", + "RealRush", + "Riccardo Centra", + "Rob Alexander", + "Roberto Linares Duran ", + "Ronal Sorto", + "Rwoo", + "Ryan", + "S W", + "Safia Suliman", + "Sajeev Shanmuganandarajah", + "Sam Farrar", + "Sam Smith", + "Sami Awadh", + "Samuel N", + "Sarthak Singhal", + "Scoop De Poop", + "Scott Barnett", + "Sean Kenworthy", + "Sebastián Ortega", + "Shaunpaul S Jones", + "Shawn Lesniak", + "Shengyi You", + "Simon Liebling", + "Simon Thomas", + "Sonali", + "Sonia Yu", + "Sophie G.", + "spiraldancing", + "Sree Uppalapati", + "Starry", + "Steve Dang", + "Steven Nevers", + "Steven Parra", + "Syed Akbar", + "Tadeusz Zaminsky", + "Taha", + "Takuya Tateishi", + "Thomas X", + "tld", + "Tom", + "tonio Cooper", + "Tuck Jones", + "Umar Mukthar", + "une_personne", + "UnseenStrangeStranger", + "Valentin Nemcev", + "VicM", + "Vikram", + "Ville Ding", + "Vit Vsiansky", + "Voirer", + "Will", + "William Ford", + "Winston Tg", + "Won-Jun Kuk", + "Yilun Zhang", + "Yohai Waismann", + "Yuniesky Echemendia", + "Zoltan Szabo", + "Étudiant De La Vie", + "수환 문", + "현우 박" +] +MEMBERS_TOP = [ + "Angel", + "Benjamin Taylor", + "Blacky 372", + "David Dales", + "David Longo", + "Eric Auld", + "Juan Busco", + "Leelabati Biswas", + "Michael McDonald", + "Paul Bake" +] diff --git a/addons/291119185/gui/__init__.py b/addons/291119185/gui/__init__.py new file mode 100644 index 00000000000..b61dbe53735 --- /dev/null +++ b/addons/291119185/gui/__init__.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- + +# Batch Editing Add-on for Anki +# +# Copyright (C) 2016-2019 Aristotelis P. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version, with the additions +# listed at the end of the license file that accompanied this program. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# NOTE: This program is subject to certain additional terms pursuant to +# Section 7 of the GNU Affero General Public License. You should have +# received a copy of these additional terms immediately following the +# terms and conditions of the GNU Affero General Public License that +# accompanied this program. +# +# If not, please request a copy through one of the means of contact +# listed here: . +# +# Any modifications to this file must keep this entire header intact. + +""" +Package that bundles UI components used by the add-on +(e.g. dialogs, Qt forms, other resources) +""" + +def initializeQtResources(): + """ + Load Qt resources into memory + """ + from . import resources # noqa: F401 diff --git a/addons/291119185/gui/resources/__init__.py b/addons/291119185/gui/resources/__init__.py new file mode 100644 index 00000000000..f8b00f7b4b9 --- /dev/null +++ b/addons/291119185/gui/resources/__init__.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- + +# Batch Editing Add-on for Anki +# +# Copyright (C) 2016-2019 Aristotelis P. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version, with the additions +# listed at the end of the license file that accompanied this program. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# NOTE: This program is subject to certain additional terms pursuant to +# Section 7 of the GNU Affero General Public License. You should have +# received a copy of these additional terms immediately following the +# terms and conditions of the GNU Affero General Public License that +# accompanied this program. +# +# If not, please request a copy through one of the means of contact +# listed here: . +# +# Any modifications to this file must keep this entire header intact. + +""" +Qt resources +""" + +from anki import version as anki_version + +if anki_version.startswith("2.0"): + from .anki20 import * # noqa: F401 +else: + from .anki21 import * # noqa: F401 + +QRC_PREFIX = "batch_editing" diff --git a/addons/291119185/gui/resources/anki21/__init__.py b/addons/291119185/gui/resources/anki21/__init__.py new file mode 100644 index 00000000000..88d119d104c --- /dev/null +++ b/addons/291119185/gui/resources/anki21/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# +# Batch Editing Add-on for Anki +# Copyright (C) 2016-2019 Glutanimate +# +# This file was automatically generated by Anki Add-on Builder v0.1.4 +# It is subject to the same licensing terms as the rest of the program +# (see the LICENSE file which accompanies this program). +# +# WARNING! All changes made in this file will be lost! + +""" +Initializes generated Qt forms/resources +""" + +__all__ = [ + "icons_rc" +] + +from . import icons_rc diff --git a/addons/291119185/gui/resources/anki21/icons_rc.py b/addons/291119185/gui/resources/anki21/icons_rc.py new file mode 100644 index 00000000000..4c86375cbf5 --- /dev/null +++ b/addons/291119185/gui/resources/anki21/icons_rc.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- + +# Resource object code +# +# Created by: The Resource Compiler for PyQt5 (Qt v5.9.5) +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore + +qt_resource_data = b"\ +\x00\x00\x03\x13\ +\x3c\ +\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ +\x30\x22\x20\x3f\x3e\x3c\x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\ +\x79\x65\x72\x5f\x31\x5f\x31\x5f\x22\x20\x73\x74\x79\x6c\x65\x3d\ +\x22\x65\x6e\x61\x62\x6c\x65\x2d\x62\x61\x63\x6b\x67\x72\x6f\x75\ +\x6e\x64\x3a\x6e\x65\x77\x20\x30\x20\x30\x20\x31\x36\x20\x31\x36\ +\x3b\x22\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\x31\x22\ +\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\x22\x30\x20\x30\x20\x31\x36\ +\x20\x31\x36\x22\x20\x78\x6d\x6c\x3a\x73\x70\x61\x63\x65\x3d\x22\ +\x70\x72\x65\x73\x65\x72\x76\x65\x22\x20\x78\x6d\x6c\x6e\x73\x3d\ +\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\ +\x72\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\x22\x20\x78\x6d\x6c\ +\x6e\x73\x3a\x78\x6c\x69\x6e\x6b\x3d\x22\x68\x74\x74\x70\x3a\x2f\ +\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x31\x39\x39\x39\ +\x2f\x78\x6c\x69\x6e\x6b\x22\x3e\x3c\x70\x61\x74\x68\x20\x64\x3d\ +\x22\x4d\x37\x2e\x36\x34\x36\x2c\x31\x33\x2e\x36\x34\x36\x6c\x30\ +\x2e\x37\x30\x37\x2c\x30\x2e\x37\x30\x37\x6c\x36\x2d\x36\x63\x31\ +\x2e\x35\x37\x33\x2d\x31\x2e\x35\x37\x33\x2c\x31\x2e\x35\x37\x33\ +\x2d\x34\x2e\x31\x33\x34\x2c\x30\x2d\x35\x2e\x37\x30\x37\x63\x2d\ +\x31\x2e\x35\x37\x34\x2d\x31\x2e\x35\x37\x34\x2d\x34\x2e\x31\x33\ +\x33\x2d\x31\x2e\x35\x37\x34\x2d\x35\x2e\x37\x30\x37\x2c\x30\x6c\ +\x2d\x36\x2e\x35\x2c\x36\x2e\x35\x20\x20\x63\x2d\x31\x2e\x32\x39\ +\x38\x2c\x31\x2e\x32\x39\x38\x2d\x31\x2e\x32\x39\x38\x2c\x33\x2e\ +\x34\x30\x39\x2c\x30\x2c\x34\x2e\x37\x30\x37\x63\x30\x2e\x36\x34\ +\x38\x2c\x30\x2e\x36\x34\x38\x2c\x31\x2e\x35\x30\x31\x2c\x30\x2e\ +\x39\x37\x34\x2c\x32\x2e\x33\x35\x34\x2c\x30\x2e\x39\x37\x34\x73\ +\x31\x2e\x37\x30\x35\x2d\x30\x2e\x33\x32\x35\x2c\x32\x2e\x33\x35\ +\x34\x2d\x30\x2e\x39\x37\x34\x6c\x36\x2d\x36\x20\x20\x43\x31\x33\ +\x2e\x33\x34\x39\x2c\x37\x2e\x33\x35\x38\x2c\x31\x33\x2e\x36\x32\ +\x31\x2c\x36\x2e\x37\x2c\x31\x33\x2e\x36\x32\x31\x2c\x36\x73\x2d\ +\x30\x2e\x32\x37\x32\x2d\x31\x2e\x33\x35\x38\x2d\x30\x2e\x37\x36\ +\x38\x2d\x31\x2e\x38\x35\x34\x63\x2d\x30\x2e\x39\x39\x2d\x30\x2e\ +\x39\x39\x2d\x32\x2e\x37\x31\x37\x2d\x30\x2e\x39\x39\x2d\x33\x2e\ +\x37\x30\x37\x2c\x30\x6c\x2d\x35\x2e\x35\x2c\x35\x2e\x35\x6c\x30\ +\x2e\x37\x30\x37\x2c\x30\x2e\x37\x30\x37\x6c\x35\x2e\x35\x2d\x35\ +\x2e\x35\x20\x20\x63\x30\x2e\x36\x31\x31\x2d\x30\x2e\x36\x31\x31\ +\x2c\x31\x2e\x36\x38\x32\x2d\x30\x2e\x36\x31\x31\x2c\x32\x2e\x32\ +\x39\x33\x2c\x30\x43\x31\x32\x2e\x34\x35\x32\x2c\x35\x2e\x31\x35\ +\x39\x2c\x31\x32\x2e\x36\x32\x31\x2c\x35\x2e\x35\x36\x36\x2c\x31\ +\x32\x2e\x36\x32\x31\x2c\x36\x73\x2d\x30\x2e\x31\x36\x39\x2c\x30\ +\x2e\x38\x34\x31\x2d\x30\x2e\x34\x37\x35\x2c\x31\x2e\x31\x34\x36\ +\x6c\x2d\x36\x2c\x36\x20\x20\x63\x2d\x30\x2e\x39\x30\x38\x2c\x30\ +\x2e\x39\x30\x38\x2d\x32\x2e\x33\x38\x35\x2c\x30\x2e\x39\x30\x38\ +\x2d\x33\x2e\x32\x39\x33\x2c\x30\x73\x2d\x30\x2e\x39\x30\x38\x2d\ +\x32\x2e\x33\x38\x35\x2c\x30\x2d\x33\x2e\x32\x39\x33\x6c\x36\x2e\ +\x35\x2d\x36\x2e\x35\x63\x31\x2e\x31\x38\x34\x2d\x31\x2e\x31\x38\ +\x34\x2c\x33\x2e\x31\x30\x39\x2d\x31\x2e\x31\x38\x34\x2c\x34\x2e\ +\x32\x39\x33\x2c\x30\x73\x31\x2e\x31\x38\x34\x2c\x33\x2e\x31\x30\ +\x39\x2c\x30\x2c\x34\x2e\x32\x39\x33\x4c\x37\x2e\x36\x34\x36\x2c\ +\x31\x33\x2e\x36\x34\x36\x20\x20\x7a\x22\x2f\x3e\x3c\x2f\x73\x76\ +\x67\x3e\ +" + +qt_resource_name = b"\ +\x00\x0d\ +\x0a\xa5\x9b\x87\ +\x00\x62\ +\x00\x61\x00\x74\x00\x63\x00\x68\x00\x2d\x00\x65\x00\x64\x00\x69\x00\x74\x00\x69\x00\x6e\x00\x67\ +\x00\x05\ +\x00\x6f\xa6\x53\ +\x00\x69\ +\x00\x63\x00\x6f\x00\x6e\x00\x73\ +\x00\x0a\ +\x07\x96\x4d\x87\ +\x00\x61\ +\x00\x74\x00\x74\x00\x61\x00\x63\x00\x68\x00\x2e\x00\x73\x00\x76\x00\x67\ +" + +qt_resource_struct_v1 = b"\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ +\x00\x00\x00\x20\x00\x02\x00\x00\x00\x01\x00\x00\x00\x03\ +\x00\x00\x00\x30\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ +" + +qt_resource_struct_v2 = b"\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ +\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ +\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x20\x00\x02\x00\x00\x00\x01\x00\x00\x00\x03\ +\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x30\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ +\x00\x00\x01\x6b\x19\x7c\xa2\xd8\ +" + +qt_version = QtCore.qVersion().split('.') +if qt_version < ['5', '8', '0']: + rcc_version = 1 + qt_resource_struct = qt_resource_struct_v1 +else: + rcc_version = 2 + qt_resource_struct = qt_resource_struct_v2 + +def qInitResources(): + QtCore.qRegisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) + +def qCleanupResources(): + QtCore.qUnregisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) + +qInitResources() diff --git a/addons/291119185/main.py b/addons/291119185/main.py new file mode 100644 index 00000000000..495c28d66f5 --- /dev/null +++ b/addons/291119185/main.py @@ -0,0 +1,245 @@ +# -*- coding: utf-8 -*- + +# Batch Editing Add-on for Anki +# +# Copyright (C) 2016-2019 Aristotelis P. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version, with the additions +# listed at the end of the license file that accompanied this program. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# NOTE: This program is subject to certain additional terms pursuant to +# Section 7 of the GNU Affero General Public License. You should have +# received a copy of these additional terms immediately following the +# terms and conditions of the GNU Affero General Public License that +# accompanied this program. +# +# If not, please request a copy through one of the means of contact +# listed here: . +# +# Any modifications to this file must keep this entire header intact. + +""" +Initializes add-on components. +""" + +from __future__ import (absolute_import, division, + print_function, unicode_literals) + +import os +import tempfile + +from aqt.qt import * +from aqt.utils import tooltip, askUser, getFile + +from anki.hooks import addHook +from anki.lang import _ +from anki import version as anki_version + +from .gui import initializeQtResources + +ANKI20 = anki_version.startswith("2.0") +unicode = str if not ANKI20 else unicode + +initializeQtResources() + + +class BatchEditDialog(QDialog): + """Browser batch editing dialog""" + + def __init__(self, browser, nids): + QDialog.__init__(self, parent=browser) + self.browser = browser + self.nids = nids + self._setupUi() + + def _setupUi(self): + tlabel = QLabel("Content to add to or replace with:") + image_btn = QPushButton(clicked=self._insertMedia) + image_btn.setIcon(QIcon(":/batch-editing/icons/attach.svg")) + image_btn.setToolTip( + "Insert a media file reference (e.g. to an image)") + press_action = QAction(self, triggered=image_btn.animateClick) + press_action.setShortcut(QKeySequence(_("Alt+i"))) + image_btn.addAction(press_action) + top_hbox = QHBoxLayout() + top_hbox.addWidget(tlabel) + top_hbox.insertStretch(1, stretch=1) + top_hbox.addWidget(image_btn) + + self.tedit = QPlainTextEdit() + self.tedit.setTabChangesFocus(True) + + flabel = QLabel("In this field:") + self.fsel = QComboBox() + fields = self._getFields() + self.fsel.addItems(fields) + f_hbox = QHBoxLayout() + f_hbox.addWidget(flabel) + f_hbox.addWidget(self.fsel) + f_hbox.setAlignment(Qt.AlignLeft) + + button_box = QDialogButtonBox(Qt.Horizontal, self) + adda_btn = button_box.addButton("Add &after", + QDialogButtonBox.ActionRole) + addb_btn = button_box.addButton("Add &before", + QDialogButtonBox.ActionRole) + replace_btn = button_box.addButton("&Replace", + QDialogButtonBox.ActionRole) + close_btn = button_box.addButton("&Cancel", + QDialogButtonBox.RejectRole) + adda_btn.setToolTip("Add after existing field contents") + addb_btn.setToolTip("Add before existing field contents") + replace_btn.setToolTip("Replace existing field contents") + adda_btn.clicked.connect(lambda state, x="adda": self.onConfirm(x)) + addb_btn.clicked.connect(lambda state, x="addb": self.onConfirm(x)) + replace_btn.clicked.connect( + lambda state, x="replace": self.onConfirm(x)) + close_btn.clicked.connect(self.close) + + self.cb_html = QCheckBox(self) + self.cb_html.setText("Insert as HTML") + self.cb_html.setChecked(False) + s = QShortcut(QKeySequence(_("Alt+H")), + self, activated=lambda: self.cb_html.setChecked(True)) + + bottom_hbox = QHBoxLayout() + bottom_hbox.addWidget(self.cb_html) + bottom_hbox.addWidget(button_box) + + vbox_main = QVBoxLayout() + vbox_main.addLayout(top_hbox) + vbox_main.addWidget(self.tedit) + vbox_main.addLayout(f_hbox) + vbox_main.addLayout(bottom_hbox) + self.setLayout(vbox_main) + self.tedit.setFocus() + self.setMinimumWidth(540) + self.setMinimumHeight(400) + self.setWindowTitle("Batch Edit Selected Notes") + + def _getFields(self): + nid = self.nids[0] + mw = self.browser.mw + model = mw.col.getNote(nid).model() + fields = mw.col.models.fieldNames(model) + return fields + + def _insertMedia(self): + media_file = self._getClip() + if not media_file: + media_file = self._chooseFile() + if not media_file: + return + html = self.browser.editor._addMedia(media_file, canDelete=True) + # need to unescape images again: + html = self.browser.mw.col.media.escapeImages(html, unescape=True) + current = self.tedit.toPlainText() + new = [] + if current: + # avoid duplicate newlines + new = current.strip('\n').split('\n') + [html] + new = "\n".join(new) + else: + new = html + self.tedit.setPlainText(new) + + def _chooseFile(self): + key = (_("Media") + + " (*.jpg *.png *.gif *.tiff *.svg *.tif *.jpeg " + + "*.mp3 *.ogg *.wav *.avi *.ogv *.mpg *.mpeg *.mov *.mp4 " + + "*.mkv *.ogx *.ogv *.oga *.flv *.swf *.flac)") + return getFile(self, _("Add Media"), None, key, key="media") + + def _getClip(self): + clip = QApplication.clipboard() + if not clip or not clip.mimeData().imageData(): + return False + handle, image_path = tempfile.mkstemp(suffix='.png') + clip.image().save(image_path) + clip.clear() + if os.stat(image_path).st_size == 0: + return False + return unicode(image_path) + + def onConfirm(self, mode): + browser = self.browser + nids = self.nids + fld = self.fsel.currentText() + text = self.tedit.toPlainText() + isHtml = self.cb_html.isChecked() + if mode == "replace": + q = (u"This will replace the contents of the '{0}' field " + u"in {1} selected note(s). Proceed?").format(fld, len(nids)) + if not askUser(q, parent=self): + return + batchEditNotes(browser, mode, nids, fld, text, isHtml=isHtml) + self.close() + + +def batchEditNotes(browser, mode, nids, fld, html, isHtml=False): + if not isHtml: + # convert newlines to
elms + html = html.replace('\n', '
') + mw = browser.mw + mw.checkpoint("batch edit") + mw.progress.start() + browser.model.beginReset() + cnt = 0 + for nid in nids: + note = mw.col.getNote(nid) + if fld in note: + content = note[fld] + if isHtml: + spacer = "\n" + breaks = (spacer) + else: + breaks = ("
", "
", "
", "
") + spacer = "
" + if mode == "adda": + if content.endswith(breaks): + spacer = "" + note[fld] += spacer + html + elif mode == "addb": + if content.startswith(breaks): + spacer = "" + note[fld] = html + spacer + content + elif mode == "replace": + note[fld] = html + cnt += 1 + note.flush() + browser.model.endReset() + mw.requireReset() + mw.progress.finish() + mw.reset() + tooltip("Updated {0} notes.".format(cnt), parent=browser) + + +def onBatchEdit(browser): + nids = browser.selectedNotes() + if not nids: + tooltip("No cards selected.") + return + dialog = BatchEditDialog(browser, nids) + dialog.exec_() + + +def setupMenu(browser): + menu = browser.form.menuEdit + menu.addSeparator() + a = menu.addAction('Batch Edit...') + a.setShortcut(QKeySequence("Ctrl+Alt+B")) + a.triggered.connect(lambda _, b=browser: onBatchEdit(b)) + + +addHook("browser.setupMenus", setupMenu) diff --git a/addons/291119185/manifest.json b/addons/291119185/manifest.json new file mode 100644 index 00000000000..8be22d2c290 --- /dev/null +++ b/addons/291119185/manifest.json @@ -0,0 +1,12 @@ +{ + "name": "Batch Editing", + "package": "291119185", + "ankiweb_id": "291119185", + "author": "Glutanimate", + "version": "v0.3.0", + "homepage": "https://github.com/glutanimate/batch-editing", + "conflicts": [ + "batch_editing" + ], + "mod": 1559500727 +} \ No newline at end of file diff --git a/addons/291119185/meta.json b/addons/291119185/meta.json new file mode 100644 index 00000000000..378670659ae --- /dev/null +++ b/addons/291119185/meta.json @@ -0,0 +1 @@ +{"name": "Batch Editing", "mod": 1560116344, "conflicts": ["batch_editing"]} \ No newline at end of file diff --git a/anki/find.py b/anki/find.py index 5b4b66cd933..954b4ee8ac1 100644 --- a/anki/find.py +++ b/anki/find.py @@ -592,6 +592,8 @@ def fieldNames(col, downcase=True): return names def fieldNamesForNotes(col, nids): + """The list of field names of models of notes whose id belongs to + nids.""" downcasedNames = set() origNames = [] mids = col.db.list("select distinct mid from notes where id in %s" % ids2str(nids)) diff --git a/aqt/addons.py b/aqt/addons.py index 70275bee3db..6cc1f61fcb7 100644 --- a/aqt/addons.py +++ b/aqt/addons.py @@ -901,6 +901,7 @@ def __hash__(self): """ Set of characteristic of Add-ons incorporated here""" incorporatedAddonsSet = { + Addon("Batch Editing", 291119185, 1560116344, "https://github.com/glutanimate/batch-editing", "41149dbec543b019a9eb01d06d2a3c5d13b8d830"), } incorporatedAddonsDict = {**{addon.name: addon for addon in incorporatedAddonsSet if addon.name}, diff --git a/aqt/browser.py b/aqt/browser.py index f0e74fef0a0..65c23c4bd5d 100644 --- a/aqt/browser.py +++ b/aqt/browser.py @@ -446,6 +446,7 @@ def setupMenus(self): f.actionChangeModel.triggered.connect(self.onChangeModel) f.actionFindDuplicates.triggered.connect(self.onFindDupes) f.actionFindReplace.triggered.connect(self.onFindReplace) + f.actionBatchEdit.triggered.connect(self.onBatchEdit) f.actionManage_Note_Types.triggered.connect(self.mw.onNoteTypes) f.actionDelete.triggered.connect(self.deleteNotes) # cards @@ -1239,9 +1240,9 @@ def selectedNotesAsCards(self): ",".join([str(s) for s in self.selectedNotes()])) def oneModelNotes(self): - sf = self.selectedNotes() - if not sf: - return + return self.applyToSelected(self._oneModelNotes) + + def _oneModelNotes(self, sf): mods = self.col.db.scalar(""" select count(distinct mid) from notes where id in %s""" % ids2str(sf)) @@ -1480,9 +1481,9 @@ def deleteNotes(self): self._deleteNotes() def _deleteNotes(self): - nids = self.selectedNotes() - if not nids: - return + return self.applyToSelected(self.__deleteNotes) + + def __deleteNotes(self, sf): self.mw.checkpoint(_("Delete Notes")) self.model.beginReset() # figure out where to place the cursor after the deletion @@ -1653,10 +1654,10 @@ def _reposition(self): f"select id from cards where type = {CARD_NEW} and id in " + ids2str(cids)) if not cids2: return showInfo(_("Only new cards can be repositioned.")) - d = QDialog(self) - d.setWindowModality(Qt.WindowModal) + qdialog = QDialog(self) + qdialog.setWindowModality(Qt.WindowModal) frm = aqt.forms.reposition.Ui_Dialog() - frm.setupUi(d) + frm.setupUi(qdialog) (pmin, pmax) = self.col.db.first( f"select min(due), max(due) from cards where type={CARD_NEW} and odid=0") pmin = pmin or 0 @@ -1664,7 +1665,7 @@ def _reposition(self): txt = _("Queue top: %d") % pmin txt += "\n" + _("Queue bottom: %d") % pmax frm.label.setText(txt) - if not d.exec_(): + if not qdialog.exec_(): return self.model.beginReset() self.mw.checkpoint(_("Reposition")) @@ -1682,11 +1683,11 @@ def reschedule(self): self.editor.saveNow(self._reschedule) def _reschedule(self): - d = QDialog(self) - d.setWindowModality(Qt.WindowModal) + qdialog = QDialog(self) + qdialog.setWindowModality(Qt.WindowModal) frm = aqt.forms.reschedule.Ui_Dialog() - frm.setupUi(d) - if not d.exec_(): + frm.setupUi(qdialog) + if not qdialog.exec_(): return self.model.beginReset() self.mw.checkpoint(_("Reschedule")) @@ -1751,27 +1752,106 @@ def onUndoState(self, on): if on: self.form.actionUndo.setText(self.mw.form.actionUndo.text()) - # Edit: replacing + # Edit ###################################################################### + def applyToSelected(self, fun): + nids = self.selectedNotes() + if not nids: + return + return fun(nids) + + # Edit: Batch Edit + ###################################################################### + + def onBatchEdit(self): + return self.editor.saveNow(self._onBatchEdit) + + def _onBatchEdit(self): + return self.applyToSelected(self.__onBatchEdit) + + def __onBatchEdit(self, nids): + import anki.find + fields = anki.find.fieldNamesForNotes(self.mw.col, nids) + qdialog = QDialog(self) + frm = aqt.forms.batchedit.Ui_Dialog() + frm.setupUi(qdialog) + qdialog.setWindowModality(Qt.WindowModal) + frm.field.addItems(fields) + restoreGeom(qdialog, "batchedit") + frm.addBefore.clicked.connect(lambda : self.___onBatchEdit(qdialog, frm, "before", nids, fields)) + frm.addAfter.clicked.connect(lambda : self.___onBatchEdit(qdialog, frm, "after", nids, fields)) + frm.replace.clicked.connect(lambda : self.___onBatchEdit(qdialog, frm, "replace", nids, fields)) + qdialog.exec_() + + def ___onBatchEdit(self, qdialog, frm, pos, nids, fields): + saveGeom(qdialog, "batchedit") + fieldName = fields[frm.field.currentIndex()] + self.mw.checkpoint(_("Batch edit")) + self.mw.progress.start() + self.model.beginReset() + isHtml = frm.insertAsHtml.isChecked() + print(f"isHtml is {isHtml}") + html = frm.textToAdd.toPlainText() + print(f"html is {html}") + html = html.replace('\n', '
') + if not isHtml: + html = html.replace('<', '<') + html = html.replace('>', '>') + print(f"html became {html}") + cnt = 0 + for nid in nids: + note = self.mw.col.getNote(nid) + try: + content = note[fieldName] + except KeyError: + continue + if isHtml: + spacer = "\n" + breaks = (spacer) + else: + spacer = "
" + breaks = ("
", "
", "
", spacer) + if self.col.conf("newLineInBatchEdit", False): + spacer = "" + if pos == "after": + if content.endswith(breaks): + spacer = "" + note[fieldName] += spacer + html + elif pos == "before": + if content.startswith(breaks): + spacer = "" + note[fieldName] = html + spacer + content + elif pos == "replace": + note[fieldName] = html + note.flush() + cnt += 1 + + self.model.endReset() + self.mw.progress.finish() + tooltip(f"Updated {cnt} notes.", parent=self) + qdialog.reject() + + # Edit: replacing + ###################################################################### def onFindReplace(self): - self.editor.saveNow(self._onFindReplace) + return self.editor.saveNow(self._onFindReplace) def _onFindReplace(self): - sf = self.selectedNotes() - if not sf: - return + return self.applyToSelected(self.__onFindReplace) + + def __onFindReplace(self, nids): import anki.find - fields = anki.find.fieldNamesForNotes(self.mw.col, sf) - d = QDialog(self) + fields = anki.find.fieldNamesForNotes(self.mw.col, nids) + qdialog = QDialog(self) frm = aqt.forms.findreplace.Ui_Dialog() - frm.setupUi(d) - d.setWindowModality(Qt.WindowModal) + frm.setupUi(qdialog) + qdialog.setWindowModality(Qt.WindowModal) frm.field.addItems([_("All Fields")] + fields) frm.buttonBox.helpRequested.connect(self.onFindReplaceHelp) - restoreGeom(d, "findreplace") - r = d.exec_() - saveGeom(d, "findreplace") + restoreGeom(qdialog, "findreplace") + r = qdialog.exec_() + saveGeom(qdialog, "findreplace") if not r: return if frm.field.currentIndex() == 0: @@ -1782,7 +1862,7 @@ def _onFindReplace(self): self.mw.progress.start() self.model.beginReset() try: - changed = self.col.findReplace(sf, + changed = self.col.findReplace(nids, str(frm.find.text()), str(frm.replace.text()), frm.re.isChecked(), @@ -1799,9 +1879,9 @@ def _onFindReplace(self): self.mw.progress.finish() showInfo(ngettext( "%(a)d of %(b)d note updated", - "%(a)d of %(b)d notes updated", len(sf)) % { + "%(a)d of %(b)d notes updated", len(nids)) % { 'a': changed, - 'b': len(sf), + 'b': len(nids), }, parent=self) def onFindReplaceHelp(self): @@ -1814,11 +1894,11 @@ def onFindDupes(self): self.editor.saveNow(self._onFindDupes) def _onFindDupes(self): - d = QDialog(self) - self.mw.setupDialogGC(d) + qdialog = QDialog(self) + self.mw.setupDialogGC(qdialog) frm = aqt.forms.finddupes.Ui_Dialog() - frm.setupUi(d) - restoreGeom(d, "findDupes") + frm.setupUi(qdialog) + restoreGeom(qdialog, "findDupes") fields = sorted(anki.find.fieldNames(self.col, downcase=False), key=lambda x: x.lower()) frm.fields.addItems(fields) @@ -1826,15 +1906,15 @@ def _onFindDupes(self): # links frm.webView.onBridgeCmd = self.dupeLinkClicked def onFin(code): - saveGeom(d, "findDupes") - d.finished.connect(onFin) + saveGeom(qdialog, "findDupes") + qdialog.finished.connect(onFin) def onClick(): field = fields[frm.fields.currentIndex()] self.duplicatesReport(frm.webView, field, frm.search.text(), frm) search = frm.buttonBox.addButton( _("Search"), QDialogButtonBox.ActionRole) search.clicked.connect(onClick) - d.show() + qdialog.show() def duplicatesReport(self, web, fname, search, frm): self.mw.progress.start() diff --git a/aqt/editor.py b/aqt/editor.py index da5802ac15e..f353e6419c8 100644 --- a/aqt/editor.py +++ b/aqt/editor.py @@ -395,7 +395,7 @@ def saveNow(self, callback, keepFocus=False): self.mw.progress.timer(10, callback, False) return self.saveTags() - self.web.evalWithCallback("saveNow(%d)" % keepFocus, lambda res: callback()) + self.web.evalWithCallback(f"saveNow({keepFocus})", lambda res: callback()) def checkValid(self): cols = [] diff --git a/aqt/preferences.py b/aqt/preferences.py index 32901cce08c..2589219b91f 100644 --- a/aqt/preferences.py +++ b/aqt/preferences.py @@ -272,6 +272,7 @@ def updateOneOption(self, name, default=False, check=True, sync=True): storeValue[name] = value extraOptions = [ + ("newLineInBatchEdit",), ] def setupExtra(self): diff --git a/aqt/utils.py b/aqt/utils.py index b2dae0cb76e..e939cd2eef2 100644 --- a/aqt/utils.py +++ b/aqt/utils.py @@ -132,6 +132,13 @@ def askUser(text, parent=None, help="", defaultno=False, msgfunc=None, \ class ButtonedDialog(QMessageBox): def __init__(self, text, buttons, parent=None, help="", title="Anki"): + """ + text -- Question to explain the user what they can answer + buttons -- A list of message to show on each buttons + parent -- The window from which this is opens + help -- A message to show if user click on «help» + title -- Title of the window + """ QMessageBox.__init__(self, parent) self.buttons = [] self.setWindowTitle(title) @@ -164,6 +171,16 @@ def setDefault(self, idx): self.setDefaultButton(self.buttons[idx]) def askUserDialog(text, buttons, parent=None, help="", title="Anki"): + """ + Return a window asking the user to click on one of the buttons of Buttons. + You should use .run to open it, and it returns the text of the button pressed or None. + + text -- Question to explain the user what they can answer + buttons -- A list of message to show on each buttons + parent -- The window from which this is opens + help -- A message to show if user click on «help» + title -- Title of the window + """ if not parent: parent = aqt.mw diag = ButtonedDialog(text, buttons, parent, help, title=title) @@ -210,6 +227,18 @@ def helpRequested(self): def getText(prompt, parent=None, help=None, edit=None, default="", title="Anki", geomKey=None, **kwargs): + """ + A pair, with: + * a text, entered by the user (or empty string) + * value returned by the window. It's falsy if the window was closed and not validated. + + text -- Text to explain the user what they can answer + parent -- The window from which this is opens. By default, aqt.mw + help -- A message to show if user click on «help» + edit -- Which editor to use. By default QLineEdit. No other is used in anki's code. + default -- Default value of the text + title -- Title of the window + """ if not parent: parent = aqt.mw.app.activeWindow() or aqt.mw d = GetTextDialog(parent, prompt, help=help, edit=edit, @@ -223,7 +252,8 @@ def getText(prompt, parent=None, help=None, edit=None, default="", return (str(d.l.text()), ret) def getOnlyText(*args, **kwargs): - """A text asked to the user, or the empty string.""" + """A text asked to the user, or the empty string. Same as getText, + without second element.""" (s, r) = getText(*args, **kwargs) if r: return s diff --git a/designer/batchedit.ui b/designer/batchedit.ui new file mode 100644 index 00000000000..8cd8bec5635 --- /dev/null +++ b/designer/batchedit.ui @@ -0,0 +1,156 @@ + + + Dialog + + + + 0 + 0 + 821 + 572 + + + + Dialog + + + + + 0 + 0 + 801 + 561 + + + + + + + Content to add to or replace with: + + + + + + + + + + + + In this field: + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Insert as HTML + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel + + + + + + + Add &after + + + + + + + Add &before + + + + + + + &Replace + + + + + + + + + + + + buttonBox + accepted() + Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/designer/browser.ui b/designer/browser.ui index 71ddbf188e3..b0115a9d00e 100644 --- a/designer/browser.ui +++ b/designer/browser.ui @@ -236,7 +236,7 @@ 0 0 750 - 22 + 20 @@ -311,6 +311,7 @@ + @@ -599,6 +600,14 @@ Ctrl+K + + + Batch &Edit Notes + + + Ctrl+Alt+B + + diff --git a/designer/preferences.ui b/designer/preferences.ui index 0009a7bf405..6b1f18ba94b 100644 --- a/designer/preferences.ui +++ b/designer/preferences.ui @@ -488,6 +488,23 @@ true + + + + + Note with no card: create card 1 instead of deleting the note + + + true + + + + + + + Add new line when batch editing fields + + diff --git a/difference.md b/difference.md index eeaf6f0f9f3..9be31eacab4 100644 --- a/difference.md +++ b/difference.md @@ -2,3 +2,9 @@ This files list the difference between regular anki and this forked version. It also lists the different options in the Preferences's extra page. +## Batch Edit (291119185) +Allow to make the same edit to multiple cards. Either changing a +field, or adding text after/before it. + +In preferences, you can decide whether you add a new line between the +old text and the added one. From ea4c1df7b057aeb2e655e91472da55ce4a13d543 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Mon, 17 Jun 2019 07:50:43 +0200 Subject: [PATCH 34/68] Merge --- addons/1262882834/__init__.py | 3 + addons/1262882834/config.json | 2 + addons/1262882834/config.md | 1 + addons/1262882834/config.py | 24 +++++++ addons/1262882834/deckPrefix21.py | 101 ++++++++++++++++++++++++++++++ aqt/addons.py | 1 + aqt/browser.py | 53 ++++++++++++++++ designer/browser.ui | 31 ++++++++- difference.md | 6 ++ 9 files changed, 221 insertions(+), 1 deletion(-) create mode 100644 addons/1262882834/__init__.py create mode 100644 addons/1262882834/config.json create mode 100644 addons/1262882834/config.md create mode 100644 addons/1262882834/config.py create mode 100644 addons/1262882834/deckPrefix21.py diff --git a/addons/1262882834/__init__.py b/addons/1262882834/__init__.py new file mode 100644 index 00000000000..bd6701ec55d --- /dev/null +++ b/addons/1262882834/__init__.py @@ -0,0 +1,3 @@ +import sys +from . import deckPrefix21 + diff --git a/addons/1262882834/config.json b/addons/1262882834/config.json new file mode 100644 index 00000000000..2f90250e377 --- /dev/null +++ b/addons/1262882834/config.json @@ -0,0 +1,2 @@ +{"Shortcut: Add prefix": "Ctrl+Alt+P", + "Shortcut: Remove prefix": "Ctrl+Shift+Alt+P"} diff --git a/addons/1262882834/config.md b/addons/1262882834/config.md new file mode 100644 index 00000000000..45ed9eca317 --- /dev/null +++ b/addons/1262882834/config.md @@ -0,0 +1 @@ +Set the value to null if you don't want to introduce a shortcut. The name of the shortcut should be self explainatory. \ No newline at end of file diff --git a/addons/1262882834/config.py b/addons/1262882834/config.py new file mode 100644 index 00000000000..6b3198ed8cb --- /dev/null +++ b/addons/1262882834/config.py @@ -0,0 +1,24 @@ +from aqt import mw +configs = None +def readIfRequired(): + global configs + if configs is None: + configs = mw.addonManager.getConfig(__name__) or dict() + +def newConf(config): + global configs + configs = None + +def getConfig(s = None, default = None): + """Get the dictionnary of configs. If a name is given, return the + object with this name if it exists. + + reads if required.""" + + readIfRequired() + if s is None: + return configs + else: + return configs.get(s, default) + +mw.addonManager.setConfigUpdatedAction(__name__,newConf) diff --git a/addons/1262882834/deckPrefix21.py b/addons/1262882834/deckPrefix21.py new file mode 100644 index 00000000000..fea9d1b433b --- /dev/null +++ b/addons/1262882834/deckPrefix21.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- + +"""Copyright: Arthur Milchior arthur@milchior.fr +License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html +Select any number of cards in the card browser and add a prefix to +their deck name, or remove the left-most prefix + +To use: + +1) Open the card browser +2) Select the desired cards +3) Go to "Edit > Add prefix "/"Remove prefix" or press ctrl+alt+p/ctrl+alt+shift+p + +My main goal was: exporting some cards selected in the browser. Adding +a common prefix to all of them allows to use the usual export +procedure. Once inported, I only need to remove the prefix in order +for the card to go back to their deck. It leaves an empty deck +"prefix" which can quickly and easily be removed. +""" + +from PyQt5.QtCore import * +from PyQt5.QtWidgets import * +from PyQt5.QtGui import * +from anki.hooks import addHook +from aqt import mw +from aqt.utils import getOnlyText, tooltip +from .config import getConfig +import anki.notes + +def addPrefix(cids): + mw.checkpoint("Add prefix") + mw.progress.start() + + prefix=getOnlyText(_("Prefix to add:"), default="prefix") + # Copy notes + for cid in cids: + #print ("Found card: %s" % (cid)) + card = mw.col.getCard(cid) + did = card.odid or card.did + deckName = mw.col.decks.name(did) + deck = mw.col.decks.get(did, default=False) + assert deck + newDeckName = "%s::%s"% (prefix,deckName) + newDid = mw.col.decks.id(newDeckName,type=deck) + card.did=newDid + card.flush() + + # Reset collection and main window + mw.col.decks.flush() + mw.progress.finish() + mw.col.reset() + mw.reset() + tooltip(_("""Prefix added.""")) + +def removePrefix(cids): + mw.checkpoint("Remove prefix") + mw.progress.start() + + # Copy notes + for cid in cids: + #print( "Found card: %s" % (cid)) + card = mw.col.getCard(cid) + did = card.odid or card.did + deckName = mw.col.decks.name(did) + deck = mw.col.decks.get(did, default=False) + assert deck + newDeckName = '::'.join(mw.col.decks._path(deckName)[1:]) + newDid = mw.col.decks.id(newDeckName,type=deck) + card.did=newDid + card.flush() + + # Reset collection and main window + mw.col.decks.flush() + mw.col.reset() + mw.reset() + mw.progress.finish() + tooltip(_("""Prefix removed.""")) + + +def setupMenu(browser): + a = QAction("Add prefix", browser) + shortcut = getConfig("Shortcut: Add prefix","Ctrl+Alt+P") + if shortcut: + a.setShortcut(QKeySequence(shortcut)) + a.triggered.connect(lambda : onAddPrefix(browser)) + browser.form.menuEdit.addAction(a) + shortcut = getConfig("Shortcut: Remove prefix","Ctrl+Shift+Alt+P") + if shortcut: + a.setShortcut(QKeySequence(shortcut)) + a = QAction("Remove prefix", browser) + a.setShortcut(QKeySequence("Ctrl+Shift+Alt+P")) + a.triggered.connect(lambda : onRemovePrefix(browser)) + browser.form.menuEdit.addAction(a) + +def onAddPrefix(browser): + addPrefix(browser.selectedCards()) + +def onRemovePrefix(browser): + removePrefix(browser.selectedCards()) + +addHook("browser.setupMenus", setupMenu) diff --git a/aqt/addons.py b/aqt/addons.py index 70275bee3db..ede1ea831d2 100644 --- a/aqt/addons.py +++ b/aqt/addons.py @@ -901,6 +901,7 @@ def __hash__(self): """ Set of characteristic of Add-ons incorporated here""" incorporatedAddonsSet = { + Addon("Change cards decks prefix", 1262882834, 1550534152, "f9843693dafb4aeb2248de5faf44cf5b5fdc69ec", "https://github.com/Arthur-Milchior/anki-deck-prefix-edit"), } incorporatedAddonsDict = {**{addon.name: addon for addon in incorporatedAddonsSet if addon.name}, diff --git a/aqt/browser.py b/aqt/browser.py index f0e74fef0a0..739d7f3a96a 100644 --- a/aqt/browser.py +++ b/aqt/browser.py @@ -468,6 +468,13 @@ def setupMenus(self): f.actionTags.triggered.connect(self.onFilterButton) f.actionSidebar.triggered.connect(self.focusSidebar) f.actionCardList.triggered.connect(self.onCardList) + # decks + f.addPrefix.triggered.connect(self.addPrefix) + self.addPrefixShortcut = QShortcut(QKeySequence("Ctrl+Alt+P"), self) + self.addPrefixShortcut.activated.connect(self.addPrefix) + self.removePrefixShortcut = QShortcut(QKeySequence("Ctrl+Alt+Shift+P"), self) + self.removePrefixShortcut.activated.connect(self.removePrefix) + f.removePrefix.triggered.connect(self.removePrefix) # help f.actionGuide.triggered.connect(self.onHelp) # keyboard shortcut for shift+home/end @@ -1548,6 +1555,52 @@ def _setDeck(self): self.model.endReset() self.mw.requireReset() + def addPrefix(self): + self.mw.checkpoint("Add prefix") + self.mw.progress.start() + + prefix=getOnlyText(_("Prefix to add:"), default="prefix") + for cid in self.selectedCards(): + card = self.col.getCard(cid) + did = card.odid or card.did + deckName = self.col.decks.name(did) + deck = self.col.decks.get(did, default=False) + assert deck + newDeckName = "%s::%s"% (prefix,deckName) + newDid = self.col.decks.id(newDeckName,type=deck) + card.did=newDid + card.flush() + + # Reset collection and main window + self.col.decks.flush() + self.mw.progress.finish() + self.col.reset() + self.mw.reset() + tooltip(_("""Prefix added.""")) + + def removePrefix(self): + self.mw.checkpoint("Remove prefix") + self.mw.progress.start() + + for cid in self.selectedCards(): + card = self.col.getCard(cid) + did = card.odid or card.did + deckName = self.col.decks.name(did) + deck = self.col.decks.get(did, default=False) + assert deck + newDeckName = '::'.join(self.col.decks._path(deckName)[1:]) + newDid = self.col.decks.id(newDeckName,type=deck) + card.did=newDid + card.flush() + + # Reset collection and main window + self.col.decks.flush() + self.col.reset() + self.mw.reset() + self.mw.progress.finish() + tooltip(_("""Prefix removed.""")) + + # Tags ###################################################################### diff --git a/designer/browser.ui b/designer/browser.ui index 71ddbf188e3..8bc219b9c99 100644 --- a/designer/browser.ui +++ b/designer/browser.ui @@ -236,7 +236,7 @@ 0 0 750 - 22 + 20 @@ -315,12 +315,21 @@ + + + + + &Decks + + + + @@ -599,6 +608,26 @@ Ctrl+K + + + Add prefix + + + + + Remove prefix + + + + + Add prefix + + + + + Remove prefix + + diff --git a/difference.md b/difference.md index eeaf6f0f9f3..92f2050d111 100644 --- a/difference.md +++ b/difference.md @@ -2,3 +2,9 @@ This files list the difference between regular anki and this forked version. It also lists the different options in the Preferences's extra page. +## Add/remove deck prefix (683170394) +In the browser, you can select cards, and then do `Decks > Add +prefix`, to add the same prefix to the deck name of all of those +cards. This ensure that they all belong to a same deck, while keeping +the same deck hierarchy. `Decks > Remove prefix` allows to remove this +common prefix and thus cancel the action `Add prefix`. From c5903cdd6ad3aa6fc8460ba3c4752b8fd2623aff Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Mon, 1 Jul 2019 13:04:24 +0200 Subject: [PATCH 35/68] Change note types without sync --- addons/719871418/README.md | 43 ++++++++++++++ addons/719871418/__init__.py | 15 +++++ addons/719871418/change20.py | 106 +++++++++++++++++++++++++++++++++++ addons/719871418/meta.json | 1 + anki/models.py | 4 +- aqt/addons.py | 1 + aqt/browser.py | 19 +++---- aqt/preferences.py | 1 + designer/preferences.ui | 17 ++++++ difference.md | 6 ++ 10 files changed, 202 insertions(+), 11 deletions(-) create mode 100644 addons/719871418/README.md create mode 100644 addons/719871418/__init__.py create mode 100644 addons/719871418/change20.py create mode 100644 addons/719871418/meta.json diff --git a/addons/719871418/README.md b/addons/719871418/README.md new file mode 100644 index 00000000000..944fc4ff1f1 --- /dev/null +++ b/addons/719871418/README.md @@ -0,0 +1,43 @@ +# Changing a note type without requiring a full new sync. +## Rationale +Currently, changing a note type require to sync the entire +collection. This add-on symply removes this restriction and assume +that you, the user, will act wisely in order to avoid creating bugs. + +It means that you must sync all your devices before changing a note +type, and then sync them after the note type. I.e. you must act +exactly has you would have done without this add-on. + +## Warning +If you select a note `n` on your computer, and change its note type, +then you must not do ANYTHING with this note type on any other +computer, smartphone, etc... before you have synchronized +them. Otherwise, you risk to loose some cards, or even the note. + +## Internal +In version 2.1 + +This add-on modifies `aqt.browser.ChangeModel.accept`; the new method +call the last one. It also modifies +`anki.collection._Collection.modSchema` while +`aqt.browser.ChangeModel.accept` runs, and set it back to its original +value afterward. + +### Version 2.0 +It changes `anki.model.ModelManager.change`. + +Note for developpers +The method change, of ModelManager, is changed. It now returns the list of the new note ids of the changed note + +Update 2018-01-10: The creation date of note is preserved as much as possible (the date is incremented of a few miliseconds). The creation date of card is not preserved. + + +## Links, licence and credits + +Key |Value +------------|------------------------------------------------------------------- +Copyright | Arthur Milchior +Based on | Anki code by Damien Elmes +License | GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +Source in | https://github.com/Arthur-Milchior/anki-change-note-type.git +Addon number| [719871418](https://ankiweb.net/shared/info/719871418) diff --git a/addons/719871418/__init__.py b/addons/719871418/__init__.py new file mode 100644 index 00000000000..89a3b57de06 --- /dev/null +++ b/addons/719871418/__init__.py @@ -0,0 +1,15 @@ +from anki.collection import _Collection +from aqt.browser import ChangeModel + +oldAccept = ChangeModel.accept + +def modSchema(self, check): + pass + +def accept(self): + oldModSchema = _Collection.modSchema + _Collection.modSchema = modSchema + oldAccept(self) + _Collection.modSchema = oldModSchema + +ChangeModel.accept = accept diff --git a/addons/719871418/change20.py b/addons/719871418/change20.py new file mode 100644 index 00000000000..cbfa7decd55 --- /dev/null +++ b/addons/719871418/change20.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- +#Copyright: Arthur Milchior arthur@milchior.fr +#License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html +#Feel free to contribute to this code on https://github.com/Arthur-Milchior/anki-change-note-type +#Anki add-on number: 719871418 + +""" +"Change card's type" does not require a new upload of the database. +The only downside being that the identifier of the note change. +I.e. you lose the content provided by the «info» button (i.e. the list of dates at which you clicked on again, hard, good, easy). +Note however that the number of leech, the easyness, the date of next review, etc... are all preserved. + +Update 2018-01-10: The creation date of note is preserved as much as possible (the date is incremented of a few miliseconds). The creation date of card is not preserved. +""" +from PyQt4.QtCore import * +from PyQt4.QtGui import * +from anki.hooks import addHook +from aqt import mw +from aqt.utils import tooltip +from anki.utils import timestampID +import anki.notes +from anki.models import ModelManager + + + +def oldChange(self, m, nids, newModel, fmap, cmap): + """Change the model of the nodes in nids to newModel + + currently, fmap and cmap are null only for tests. + + keyword arguments + m -- the previous model of the notes + nids -- a list of id of notes whose model is m + newModel -- the model to which the cards must be converted + fmap -- the dictionnary sending to each fields'ord of the old model a field'ord of the new model or to None. + cmap -- the dictionnary sending to each card type's ord of the old model a card type's ord of the new model + """ + assert newModel['id'] == m['id'] or (fmap and cmap) + if fmap: + self._changeNotes(nids, newModel, fmap) + if cmap: + self._changeCards(nids, m, newModel, cmap) + self.col.genCards(nids) + +def newChange(self,m,nids,newModel,fmap,cmap): + """As change. As side effect, change nids of cards. Return new nids """ + new_nids=map(copyCards,nids) + oldChange(self,m,new_nids,newModel,fmap,cmap) + self.col.remNotes(nids) + self.col._remNotes(nids) + return new_nids + +ModelManager.change =newChange + +###Taken from Copy.py +def timestampID(db, table, t=None): + "Return a non-conflicting timestamp for table." + # be careful not to create multiple objects without flushing them, or they + # may share an ID. + t = t or intTime(1000) + while db.scalar("select id from %s where id = ?" % table, t): + t += 1 + return t +def copyCards(nids): + mw.checkpoint("Copy Notes") + mw.progress.start() + + + # Copy notes + for nid in nids: + print "Found note: %s" % (nid) + note = mw.col.getNote(nid) + model = note._model + + # Create new note + note_copy = anki.notes.Note(mw.col,model=model) + # Copy tags and fields (all model fields) from original note + note_copy.tags = note.tags + note_copy.fields = note.fields + note_copy.id = timestampID(note.col.db, "notes", note.id) + # Refresh note and add to database + note_copy.flush() + mw.col.addNote(note_copy) + nid_copy = note_copy.id + + cards_copy= note_copy.cards() + cards= note.cards() + ord_to_card = {card.ord:card for card in cards} + ord_to_card_copy = {card.ord:card for card in cards_copy} + for card in cards: + ord = card.ord + card_copy = ord_to_card_copy.get(ord) + if card_copy: + card.id=card_copy.id + card.nid = nid_copy + else: + tooltip("We copy a card which should not exists.") + card.id=timestampID(mw.col.db,"cards") + card.nid=nid_copy + card.flush() + + # Reset collection and main window + mw.col.reset() + mw.reset() + + tooltip(_("""Cards copied.""")) diff --git a/addons/719871418/meta.json b/addons/719871418/meta.json new file mode 100644 index 00000000000..0b36beceb73 --- /dev/null +++ b/addons/719871418/meta.json @@ -0,0 +1 @@ +{"name": "Change a notes type without requesting a database upload", "mod": 1560126140, "disabled": true} \ No newline at end of file diff --git a/anki/models.py b/anki/models.py index a20cc66ecbe..2d1ab4f72b7 100644 --- a/anki/models.py +++ b/anki/models.py @@ -614,7 +614,9 @@ def change(self, m, nids, newModel, fmap, cmap): fmap -- the dictionnary sending to each fields'ord of the old model a field'ord of the new model cmap -- the dictionnary sending to each card type's ord of the old model a card type's ord of the new model """ - self.col.modSchema(check=True) + from aqt import mw + if mw and not mw.pm.profile.get("changeModelWithoutFullSync", False): + self.col.modSchema(check=True) assert newModel['id'] == m['id'] or (fmap and cmap) if fmap: self._changeNotes(nids, newModel, fmap) diff --git a/aqt/addons.py b/aqt/addons.py index 70275bee3db..d2a47f9e29a 100644 --- a/aqt/addons.py +++ b/aqt/addons.py @@ -901,6 +901,7 @@ def __hash__(self): """ Set of characteristic of Add-ons incorporated here""" incorporatedAddonsSet = { + Addon("Change a notes type without requesting a database upload", 719871418, 1560313030, "8b97cfbea1b7d8bd7124d6ebe6553f36d1914823", "https://github.com/Arthur-Milchior/anki-change-note-type"), } incorporatedAddonsDict = {**{addon.name: addon for addon in incorporatedAddonsSet if addon.name}, diff --git a/aqt/browser.py b/aqt/browser.py index f0e74fef0a0..474cf42e46f 100644 --- a/aqt/browser.py +++ b/aqt/browser.py @@ -2146,16 +2146,15 @@ def accept(self): Are you sure you want to continue?""")): return self.browser.mw.checkpoint(_("Change Note Type")) - b = self.browser - b.mw.col.modSchema(check=True) - b.mw.progress.start() - b.model.beginReset() - mm = b.mw.col.models - mm.change(self.oldModel, self.nids, self.targetModel, fmap, cmap) - b.search() - b.model.endReset() - b.mw.progress.finish() - b.mw.reset() + if not self.browser.mw.pm.profile.get("changeModelWithoutFullSync", False): + self.browser.mw.col.modSchema(check=True) + self.browser.mw.progress.start() + self.browser.model.beginReset() + self.browser.mw.col.models.change(self.oldModel, self.nids, self.targetModel, fmap, cmap) + self.browser.search() + self.browser.model.endReset() + self.browser.mw.progress.finish() + self.browser.mw.reset() self.cleanup() QDialog.accept(self) diff --git a/aqt/preferences.py b/aqt/preferences.py index 32901cce08c..6f899ad9c14 100644 --- a/aqt/preferences.py +++ b/aqt/preferences.py @@ -272,6 +272,7 @@ def updateOneOption(self, name, default=False, check=True, sync=True): storeValue[name] = value extraOptions = [ + ("changeModelWithoutFullSync", False, True, False), ] def setupExtra(self): diff --git a/designer/preferences.ui b/designer/preferences.ui index 0009a7bf405..e8cd93a7cf4 100644 --- a/designer/preferences.ui +++ b/designer/preferences.ui @@ -490,6 +490,23 @@ + + + Change model without requiring a full sync + + + + + + + <html><head/><body><p>I understand that I should take care of syncing myself, or I might create bugs</p></body></html> + + + true + + + + Qt::Vertical diff --git a/difference.md b/difference.md index eeaf6f0f9f3..415d2a199dc 100644 --- a/difference.md +++ b/difference.md @@ -2,3 +2,9 @@ This files list the difference between regular anki and this forked version. It also lists the different options in the Preferences's extra page. +## Changing a note type without synchronizing (719871418) +The preference "Change model without requiring a full sync" allow you +to avoid full sync after adding/removing a field or a card type to a +note type. However, this is risky. Thus you should only do this if you +are sure that the change on all devices are already +synchronized. Otherwise, it could create a bug. From 5c81d302ab769f5160b2294eb86e27fba953a104 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Sat, 6 Jul 2019 19:12:04 +0200 Subject: [PATCH 36/68] Constant instead of importMode --- anki/importing/noteimp.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/anki/importing/noteimp.py b/anki/importing/noteimp.py index f51d1c4950f..c79b5e3a3b6 100644 --- a/anki/importing/noteimp.py +++ b/anki/importing/noteimp.py @@ -45,10 +45,13 @@ def __init__(self): # 0: update if first field matches existing note # 1: ignore if first field matches existing note # 2: import even if first field matches existing note +updateMode = 0 +ignoreMode = 1 +addMode = 2 class NoteImporter(Importer): """TODO - + keyword arguments: mapping -- A list of name of fields of model model -- to which model(note type) the note will be imported. @@ -60,7 +63,7 @@ class NoteImporter(Importer): needMapper = True needDelimiter = False allowHTML = False - importMode = 0 + importMode = updateMode def __init__(self, col, file): Importer.__init__(self, col, file) @@ -81,7 +84,7 @@ def fields(self): return 0 def initMapping(self): - """Initial mapping. + """Initial mapping. The nth element of the import is sent to nth field, if it exists to tag otherwise""" @@ -155,7 +158,7 @@ def importNotes(self, notes): " ".join(n.fields)) continue # earlier in import? - if fld0 in firsts and self.importMode != 2: + if fld0 in firsts and self.importMode != addMode: # duplicates in source file; log and ignore self.log.append(_("Appeared twice in file: %s") % fld0) @@ -172,16 +175,16 @@ def importNotes(self, notes): if fld0 == sflds[0]: # duplicate found = True - if self.importMode == 0: + if self.importMode == updateMode: data = self.updateData(n, id, sflds) if data: updates.append(data) updateLog.append(updateLogTxt % fld0) dupeCount += 1 found = True - elif self.importMode == 1: + elif self.importMode == ignoreMode: dupeCount += 1 - elif self.importMode == 2: + elif self.importMode == addMode: # allow duplicates in this case if fld0 not in dupes: # only show message once, no matter how many @@ -217,9 +220,9 @@ def importNotes(self, notes): part1 = ngettext("%d note added", "%d notes added", len(new)) % len(new) part2 = ngettext("%d note updated", "%d notes updated", self.updateCount) % self.updateCount - if self.importMode == 0: + if self.importMode == updateMode: unchanged = dupeCount - self.updateCount - elif self.importMode == 1: + elif self.importMode == ignoreMode: unchanged = dupeCount else: unchanged = 0 From 1a70d225ce26d86cbe0acee23e452aa4cdd50447 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Wed, 3 Jul 2019 22:11:20 +0200 Subject: [PATCH 37/68] Compile latex asap --- addons/769835008/README.md | 71 +++++++++++++ addons/769835008/__init__.py | 1 + addons/769835008/compileLatexEarly21.py | 130 ++++++++++++++++++++++++ addons/769835008/config.json | 3 + addons/769835008/config.md | 4 + anki/latex.py | 83 +++++++++------ anki/media.py | 17 +++- anki/notes.py | 30 ++++++ aqt/preferences.py | 1 + designer/preferences.ui | 21 +++- 10 files changed, 323 insertions(+), 38 deletions(-) create mode 100644 addons/769835008/README.md create mode 100644 addons/769835008/__init__.py create mode 100644 addons/769835008/compileLatexEarly21.py create mode 100644 addons/769835008/config.json create mode 100644 addons/769835008/config.md diff --git a/addons/769835008/README.md b/addons/769835008/README.md new file mode 100644 index 00000000000..97d608d45ad --- /dev/null +++ b/addons/769835008/README.md @@ -0,0 +1,71 @@ +# Compile all LaTeX, warn about compilation errors +## Rationale +This add-on repare three related problems with anki's compilation +process for LaTeX. + +### Errors +You never know whether your LaTeX is valid untill you actually see the +card containing the LaTeX. When this occur, you are probably learning +new cards and don't want to stop to correct your typo. + +I thus prefer to know when an error occur when I'm creating or editing +a card. When a new card is created with an error, or when I'm creating +an error while editing a note, a warning box will pop-up to warn me. + +During edition, a tooltip (yellow box) reminding you a bug exists regularly +occur. You may know that the bug is solved as soon as you don't see +the tooltip. +### Finding errors. +Furthermore, a tag #LaTeXError is added to notes with an error in +their LaTeX. To find and correct those notes, it suffices to search +for this tag. +### Synchronization +Ankiweb and smartphone can't compile LaTeX. Thus, when you create new +cards, don't click on "check media", synchronize, and use ankiweb, +ankidroid, anki on IOS, you have notes without their LaTeX. Since you +don't want to always have to remember to use "check media", you may +want to use this add-on. Since it compiles LaTeX as soon as it is +written, the image will always be generated. +## Warning +The browser does not update when a tag is added/removed. Thus you may +not trust the tag list in the browser to know whether your last edit +created/removed a bug. + +### A problem when LaTeX header change +If you change the footer/header, Anki will not recompile your LaTeX code. In +particular, if it founds an error the last time it tried to compile +your LaTeX code, it will believes the error is still present even if, +changing the header, you may actually have corrected the error. In +order to ensure Anki tries to compile again, you need either to +restart anki (it won't recall what it already tried to compile), or +edit the code. You can usually add a space or a {} in your LaTeX +without changing your output. + +### Too much images +Assume you write [$]\pi=3[/$], and a little bit later, you add [$]\pi=3.14[/$], and +a little bit later [$]\pi=3.1416[/$]. Since this add-on compile often, +the three versions of your code will have been compiled, and will have +generated LaTeX image, saved in your media folder. It takes space. So +you may still want sometime to do a "check media", to delete all of +those partial compilations. + +## Configuration (Version 2.1 only) +"warningBox" configure when should a warning box tell you when you introduce a latex error. When warning are not used, yellow box (tooltips) are used instead. Its possible values are: +* "never": i.e. always use tooltips +* "On first error" : with this option, you'll be warned each time you introduce an error in a note. That is, if a note has no LaTeX error and you introduce one, then you'll have a warning message. However, if the note already has a LaTeX error, you won't be notified again. (This is the default) + +## Internal +This change Note.flush, still calling the former Note.flush. + + + +## Links, licence and credits + +Key |Value +------------|------------------------------------------------------------------- +Copyright | Arthur Milchior +Based on | Anki code by Damien Elmes +License | GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +Source in | https://github.com/Arthur-Milchior/anki-compile-latex-early/ +Addon number| [769835008](https://ankiweb.net/shared/info/769835008) +Support me on| [![Ko-fi](https://ko-fi.com/img/Kofi_Logo_Blue.svg)](Ko-fi.com/arthurmilchior) or [![Patreon](http://www.milchior.fr/patreon.png)](https://www.patreon.com/bePatron?u=146206) diff --git a/addons/769835008/__init__.py b/addons/769835008/__init__.py new file mode 100644 index 00000000000..368a3bb2c51 --- /dev/null +++ b/addons/769835008/__init__.py @@ -0,0 +1 @@ +from . import compileLatexEarly21 diff --git a/addons/769835008/compileLatexEarly21.py b/addons/769835008/compileLatexEarly21.py new file mode 100644 index 00000000000..23dfe262a9a --- /dev/null +++ b/addons/769835008/compileLatexEarly21.py @@ -0,0 +1,130 @@ +# -*- coding: utf-8 -*- +# Copyright: Arthur Milchior arthur@milchior.fr +# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html +# Feel free to contribute to the code on https://github.com/Arthur-Milchior/anki-compile-latex-early/ +# Add-on number: 769835008 + +from anki.hooks import wrap +from aqt.utils import tooltip, showWarning +from anki.utils import checksum +from anki.notes import Note +import re +import unicodedata +import anki.notes +from aqt import mw +import os +from anki.latex import regexps, _latexFromHtml, build, _buildImg +from anki.consts import * +from anki.lang import _ + + + +def mungeQA(html, type, fields, model, data, col): + "Convert TEXT with embedded latex tags to image links. Returns the HTML and whether an error occurred." + error = False + for match in regexps['standard'].finditer(html): + link, er = _imgLink(col, match.group(1), model) + html = html.replace(match.group(), link) + error = error or er + for match in regexps['expression'].finditer(html): + link, er = _imgLink( + col, "$" + match.group(1) + "$", model) + html = html.replace(match.group(), link) + error = error or er + for match in regexps['math'].finditer(html): + link, er = _imgLink( + col, + "\\begin{displaymath}" + match.group(1) + "\\end{displaymath}", model) + html = html.replace(match.group(), link) + error = error or er + return html, error + +buggedLatex ={} + +def _imgLink(col, latex, model): + """A pair containing: + An img link for LATEX, creating if necesssary. + Whether an error occurred.""" + txt = _latexFromHtml(col, latex) + if txt in buggedLatex: + return (buggedLatex[txt],True) + if model.get("latexsvg", False): + ext = "svg" + else: + ext = "png" + + # is there an existing file? + fname = "latex-%s.%s" % (checksum(txt.encode("utf8")), ext) + link = '' % fname + if os.path.exists(fname): + return (link,False) + + # building disabled? + if not build: + return ("[latex]%s[/latex]" % latex,False) + + err = _buildImg(col, txt, fname, model) + if err: + buggedLatex[txt]=err + return (err,True) + else: + return (link,False) + +oldFlush = Note.flush +def filesInStr(self, mid, string, includeRemote=False): + """The list of media's path in the string + + Keyword arguments: + self -- the media manager + mid -- the id of the model of the note whose string is considered + string -- A string, which corresponds to a field of a note + includeRemote -- whether the list should include contents which is with http, https or ftp + """ + l = [] + model = self.col.models.get(mid) + strings = [] + someError = False + if model['type'] == MODEL_CLOZE and "{{c" in string: + # if the field has clozes in it, we'll need to expand the + # possibilities so we can render latex + strings = self._expandClozes(string) + else: + strings = [string] + for string in strings: + # handle latex + (string,error) = mungeQA(string, None, None, model, None, self.col) + someError = error or someError + # extract filenames + for reg in self.regexps: + for match in re.finditer(reg, string): + fname = match.group("fname") + isLocal = not re.match("(https?|ftp)://", fname.lower()) + if isLocal or includeRemote: + l.append(fname) + return (l,someError) + +def noteFlush(note, mod=None): + someError=False + for field in note.fields: + (_,error)=filesInStr(note.col.media, note.mid, field, note, mod) + someError = someError or error + if someError: + if note.hasTag("LaTeXError"): + tooltip("Some LaTex compilation error remains.") + else: + note.addTag("LaTeXError") + whichAlert= mw.addonManager.getConfig(__name__).get("warningBox",True) + message = "There was some LaTex compilation error." + if whichAlert.lower() == "never": + tooltip(message) + else: + showWarning(message) + else: + if note.hasTag("LaTeXError"): + note.delTag("LaTeXError") + tooltip("There are no more LaTeX error.") + oldFlush(note,mod=mod) + + +Note.flush = noteFlush +#this is a test for update diff --git a/addons/769835008/config.json b/addons/769835008/config.json new file mode 100644 index 00000000000..cab42a1c72c --- /dev/null +++ b/addons/769835008/config.json @@ -0,0 +1,3 @@ +{ + "warningBox":"on first error" +} diff --git a/addons/769835008/config.md b/addons/769835008/config.md new file mode 100644 index 00000000000..6dfaac62d73 --- /dev/null +++ b/addons/769835008/config.md @@ -0,0 +1,4 @@ +# "warningBox" +When should a warning box tell you when you introduce a latex error. When warning are not used, yellow box (tooltips) are used instead. Its possible values are: +* "never": i.e. always use tooltips +* "On first error" : with this option, you'll be warned each time you introduce an error in a note. That is, if a note has no LaTeX error and you introduce one, then you'll have a warning message. However, if the note already has a LaTeX error, you won't be notified again. (This is the default) diff --git a/anki/latex.py b/anki/latex.py index a6a81eafc7e..296c5a6dbaf 100644 --- a/anki/latex.py +++ b/anki/latex.py @@ -38,8 +38,14 @@ def stripLatex(text): text = text.replace(match.group(), "") return text -def mungeQA(html, type, fields, model, data, col): - """html, where LaTeX parts are replaced by some HTML. +def mungeQA(*args, **kwargs): + """Same as mungeQAandErr, but with html only""" + return mungeQAandErr(*args, **kwargs)[0] + +def mungeQAandErr(html, type, fields, model, data, col): + """A pair: + * Html, where LaTeX parts are replaced by some HTML. + * Whether there is an error see _imgLink docstring regarding the rules for LaTeX media. @@ -48,25 +54,27 @@ def mungeQA(html, type, fields, model, data, col): type -- not used. "q" or "a" for question and answer fields -- not used. A dictionnary containing Tags, Type(model name), Deck, Subdeck(part after last ::), card: template - name... TODO (see collection._renderQA for more info) + name... TODO (see collection._renderQA for more info) model -- the model in which is compiled the note. It deals with the header/footer, and the image file format data -- not used. [cid, nid, mid, did, ord, tags, flds] col -- the current collection. It deals with media folder """ - for match in regexps['standard'].finditer(html): - html = html.replace(match.group(), _imgLink(col, match.group(1), model)) - for match in regexps['expression'].finditer(html): - html = html.replace(match.group(), _imgLink( - col, "$" + match.group(1) + "$", model)) - for match in regexps['math'].finditer(html): - html = html.replace(match.group(), _imgLink( - col, - "\\begin{displaymath}" + match.group(1) + "\\end{displaymath}", model)) - return html + error = False + for prefix, key, suffix in {("", "standard",""), ("$", "expression", "$"), ("\\begin{displaymath}", "math", "\\end{displaymath}")}: + for match in regexps[key].finditer(html): + textToCompile = prefix+match.group(1)+suffix + link, er = _imgLink(col, textToCompile, model) + html = html.replace(match.group(), link) + error = error or er + return (html, error) + +buggedLatex ={} # Ensure that the same image is never compiled twice with the same compiler def _imgLink(col, latex, model): - """Some HTML to display instead of the LaTeX code. + """A pair: + * Some HTML to display instead of the LaTeX code. + * whether there is an error during LaTeX compilation. If some image already exists, related to this LaTeX code, an HTML code showing this image is returned. @@ -80,7 +88,7 @@ def _imgLink(col, latex, model): During the compilation the compiled file is in tmp.tex and its output (err and std) in latex_log.tex, replacing previous files of the same name. Both of those file are in the tmpdir as in - utils.py. + utils.py. Keyword arguments: col -- the current collection. It is used for the media folder (and @@ -91,6 +99,8 @@ def _imgLink(col, latex, model): """ txt = _latexFromHtml(col, latex) + if txt in buggedLatex: + return (buggedLatex[txt],True) if model.get("latexsvg", False): ext = "svg" else: @@ -100,20 +110,22 @@ def _imgLink(col, latex, model): fname = "latex-%s.%s" % (checksum(txt.encode("utf8")), ext) link = '' % fname if os.path.exists(fname): - return link + return (link, False) # building disabled? if not build: - return "[latex]%s[/latex]" % latex + return ("[latex]%s[/latex]" % latex, False) - err = _buildImg(col, txt, fname, model) + err, compilationWasTried = _buildImg(col, txt, fname, model) if err: - return err + if compilationWasTried: + buggedLatex[txt]=err + return (err, True) else: - return link + return (link, False) def _latexFromHtml(col, latex): - """Convert entities and fix newlines. + """Convert entities and fix newlines. First argument is not used. """ @@ -125,7 +137,7 @@ def _buildImg(col, latex, fname, model): """Generate an image file from latex code latex's header and foot is added as in the model. The image is - named fname and added to the media folder of this collection. + named fname and added to the media folder of this collection. The compiled file is in tmp.tex and its output (err and std) in latex_log.tex, replacing previous files of the same name. Both of @@ -134,9 +146,12 @@ def _buildImg(col, latex, fname, model): Compiles to svg if latexsvg is set to true in the model, otherwise to png. The compilation commands are given above. - In case of error, return an error message to be displayed instead - of the LaTeX document. (Note that this image is not displayed in - AnkiDroid. It is probably shown only in computer mode) + In case of error, return: * an error message to be displayed + instead of the LaTeX document. (Note that this image is not + displayed in AnkiDroid. It is probably shown only in computer + mode) + * whether it's a problem with the LaTeX (otherwise, it's a + compilation problem, and it may be worth trying again) Keyword arguments: col -- the current collection. It deals with media folder @@ -144,6 +159,7 @@ def _buildImg(col, latex, fname, model): fname -- the name given to the generated png model -- the model in which is compiled the note. It deals with the header/footer, and the image file format + """ # add header/footer latex = (model["latexPre"] + "\n" + @@ -157,10 +173,11 @@ def _buildImg(col, latex, fname, model): # don't mind if the sequence is only part of a command bad_re = "\\" + bad + "[^a-zA-Z]" if re.search(bad_re, tmplatex): - return _("""\ + return (_("""\ + For security reasons, '%s' is not allowed on cards. You can still use \ it by placing the command in a different package, and importing that \ -package in the LaTeX header instead.""") % bad +package in the LaTeX header instead.""") % bad, True) # commands to use? if model.get("latexsvg", False): @@ -187,19 +204,21 @@ def _buildImg(col, latex, fname, model): return _errMsg(latexCmd[0], texpath) # add the image to the media folder shutil.copyfile(png, os.path.join(mdir, fname)) - return + return "", True finally: os.chdir(oldcwd) log.close() def _errMsg(type, texpath): - """An error message, in html, concerning LaTeX compilation. + """A pair with: + * an error message, in html, concerning LaTeX compilation. + * whether compilation at least started This message contains LaTeX outputs if it exists, or a message asking whether the program latex and dvipng/dvisvgm are installed. Keyword arguments - type -- the (begin of the) executed command + type -- the (begin of the) executed command texpath -- the path to the (temporary) file which was compiled """ msg = (_("Error executing %s.") % type) + "
" @@ -210,9 +229,11 @@ def _errMsg(type, texpath): if not log: raise Exception() msg += "
" + html.escape(log) + "
" + compilationWasTried = True except: msg += _("Have you installed latex and dvipng/dvisvgm?") - return msg + compilationWasTried = False + return msg, compilationWasTried # setup q/a filter addHook("mungeQA", mungeQA) diff --git a/anki/media.py b/anki/media.py index 5377af61974..208ade8d0bd 100644 --- a/anki/media.py +++ b/anki/media.py @@ -30,7 +30,7 @@ from anki.utils import checksum, isWin, isMac from anki.db import DB, DBError from anki.consts import * -from anki.latex import mungeQA +from anki.latex import mungeQAandErr from anki.lang import _ class MediaManager: @@ -238,8 +238,12 @@ def repl(match): # String manipulation ########################################################################## - def filesInStr(self, mid, string, includeRemote=False): - """The list of media's path in the string. + def filesInStr(self, *args, **kwargs): + """The list of media's path in the string.""" + return self.filesInStrOrErr(*args, **kwargs)[0] + + def filesInStrOrErr(self, mid, string, includeRemote=False): + """The list of media's path in the string. Medias starting with _ are treated as any media. @@ -252,11 +256,13 @@ def filesInStr(self, mid, string, includeRemote=False): Keyword arguments: mid -- the id of the model of the note whose string is considered string -- A string, which corresponds to a field of a note + note -- the note includeRemote -- whether the list should include contents which is with http, https or ftp """ l = [] model = self.col.models.get(mid) strings = [] + someError = False # whether a LaTeX error was found if model['type'] == MODEL_CLOZE and "{{c" in string: # if the field has clozes in it, we'll need to expand the # possibilities so we can render latex @@ -265,7 +271,8 @@ def filesInStr(self, mid, string, includeRemote=False): strings = [string] for string in strings: # handle latex - string = mungeQA(string, None, None, model, None, self.col) + (string, error) = mungeQAandErr(string, None, None, model, None, self.col) + someError = error or someError # extract filenames for reg in self.regexps: for match in re.finditer(reg, string): @@ -273,7 +280,7 @@ def filesInStr(self, mid, string, includeRemote=False): isLocal = not re.match("(https?|ftp)://", fname.lower()) if isLocal or includeRemote: l.append(fname) - return l + return (l, someError) def _expandClozes(self, string): """The list of all strings, where the clozes are expanded. diff --git a/anki/notes.py b/anki/notes.py index b6ae365ebab..c3e01b03e49 100644 --- a/anki/notes.py +++ b/anki/notes.py @@ -4,6 +4,7 @@ from anki.utils import fieldChecksum, intTime, \ joinFields, splitFields, stripHTMLMedia, timestampID, guid64 +from aqt.utils import tooltip class Note: @@ -78,6 +79,32 @@ def load(self): self._fmap = self.col.models.fieldMap(self._model) self.scm = self.col.scm + def tagTex(self, mod = None): + """Add tag LaTeXError if the note has a LaTeX error. Remove it + otherwise. + + Alert when an error appear, tooltip when it disappears. + + """ + someError=False + for field in self.fields: + error = self.col.media.filesInStrOrErr(self.mid, field)[1] + someError = someError or error + from aqt import mw + if someError: + if self.hasTag("LaTeXError"): + if mw:#in case of test, no tooltip + tooltip("Some LaTex compilation error remains.") + else: + self.addTag("LaTeXError") + if mw: + tooltip("There was some LaTex compilation error.") + else: + if self.hasTag("LaTeXError"): + self.delTag("LaTeXError") + if mw: + tooltip("There are no more LaTeX error.") + def flush(self, mod=None): """If fields or tags have changed, write changes to disk. @@ -92,6 +119,9 @@ def flush(self, mod=None): mod -- A modification timestamp""" assert self.scm == self.col.scm self._preFlush() + from aqt import mw + if self.col.conf.get("compileLaTeX", True): + self.tagTex(mod) sfld = stripHTMLMedia(self.fields[self.col.models.sortIdx(self._model)]) tags = self.stringTags() fields = self.joinedFields() diff --git a/aqt/preferences.py b/aqt/preferences.py index 32901cce08c..73be916760d 100644 --- a/aqt/preferences.py +++ b/aqt/preferences.py @@ -272,6 +272,7 @@ def updateOneOption(self, name, default=False, check=True, sync=True): storeValue[name] = value extraOptions = [ + ("compileLaTeX", ), ] def setupExtra(self): diff --git a/designer/preferences.ui b/designer/preferences.ui index 0009a7bf405..2feb330d799 100644 --- a/designer/preferences.ui +++ b/designer/preferences.ui @@ -7,7 +7,7 @@ 0 0 423 - 508 + 552
@@ -20,7 +20,7 @@ Qt::StrongFocus - 0 + 3 @@ -489,6 +489,23 @@
+ + + + Compile LaTeX as soon as it is written + + + true + + + + + + + Adding tag LaTeXError to cards with faulty notes. + + + From f26ad333093f241b89e51ccb8f8628b8ed83ce73 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Mon, 17 Jun 2019 09:36:34 +0200 Subject: [PATCH 38/68] Copy note --- addons/1566928056/LICENSE | 674 ++++++++++++++++++++++++++++++++++ addons/1566928056/README.md | 32 ++ addons/1566928056/__init__.py | 1 + addons/1566928056/config.json | 3 + addons/1566928056/config.md | 1 + addons/1566928056/config.py | 18 + addons/1566928056/copyNote.py | 92 +++++ addons/1566928056/meta.json | 1 + addons/1566928056/zipping | 1 + anki/notes.py | 31 ++ anki/utils.py | 11 +- aqt/addons.py | 1 + aqt/browser.py | 20 + aqt/preferences.py | 2 + designer/browser.ui | 11 +- designer/preferences.ui | 21 ++ difference.md | 12 + 17 files changed, 928 insertions(+), 4 deletions(-) create mode 100644 addons/1566928056/LICENSE create mode 100644 addons/1566928056/README.md create mode 100644 addons/1566928056/__init__.py create mode 100644 addons/1566928056/config.json create mode 100644 addons/1566928056/config.md create mode 100644 addons/1566928056/config.py create mode 100644 addons/1566928056/copyNote.py create mode 100644 addons/1566928056/meta.json create mode 100644 addons/1566928056/zipping diff --git a/addons/1566928056/LICENSE b/addons/1566928056/LICENSE new file mode 100644 index 00000000000..9cecc1d4669 --- /dev/null +++ b/addons/1566928056/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {one line to give the program's name and a brief idea of what it does.} + Copyright (C) {year} {name of author} + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + {project} Copyright (C) {year} {fullname} + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/addons/1566928056/README.md b/addons/1566928056/README.md new file mode 100644 index 00000000000..d7eec42d46e --- /dev/null +++ b/addons/1566928056/README.md @@ -0,0 +1,32 @@ +# Copy notes + +The copy can have only new cards. Or they can have exactly the same +intervals, ease, etc... than the original card. + +To copy a note: +1. Open the card browser +2. Select the desired notes (at least one card by note) +3. Go to "Edit > Copy and set to new" (Ctrl+C) or "Edit > Copy notes, + keep interval" (Ctrl+Shift+C) + +The copy preserve fields, tags, decks. + +## Warning +There is a potential caveat, which should not occur often nor have +real consequences. You should note that empty cards are not copied. If +you don't know what it means, you probably doesn't need to worry about +this. + +## Configuration +"Preserve creation time": as indicated by the name, if it's true, the card and note's creation time are preserved. Otherwise, it is set to the time of the copy. + +## Links, licence and credits + +Key |Value +------------|------------------------------------------------------------------- +Copyright |Arthur Milchior +Based on |Kealan Hobelmann's addon 396494452 (for anki 2.0) +Based on |Anki code by Damien Elmes +License |GNU AGPL, version 3 or later; http|//www.gnu.org/licenses/agpl.html +Source in |https://github.com/Arthur-Milchior/anki-copy-note +Addon number| [1566928056](https://ankiweb.net/shared/info/1566928056) diff --git a/addons/1566928056/__init__.py b/addons/1566928056/__init__.py new file mode 100644 index 00000000000..17c6bace589 --- /dev/null +++ b/addons/1566928056/__init__.py @@ -0,0 +1 @@ +from . import copyNote diff --git a/addons/1566928056/config.json b/addons/1566928056/config.json new file mode 100644 index 00000000000..66407e615de --- /dev/null +++ b/addons/1566928056/config.json @@ -0,0 +1,3 @@ +{ + "Preserve creation time" : true +} diff --git a/addons/1566928056/config.md b/addons/1566928056/config.md new file mode 100644 index 00000000000..2c8c41401e4 --- /dev/null +++ b/addons/1566928056/config.md @@ -0,0 +1 @@ +"Preserve creation time": as indicated by the name, if it's true, the card and note's creation time are preserved. Otherwise, it is set to the time of the copy. \ No newline at end of file diff --git a/addons/1566928056/config.py b/addons/1566928056/config.py new file mode 100644 index 00000000000..8fba913fc8c --- /dev/null +++ b/addons/1566928056/config.py @@ -0,0 +1,18 @@ +import aqt +from aqt import mw +from anki.hooks import addHook, remHook + + +singleList = False +userOption = None +def getUserOption(): + global userOption + if userOption is None: + userOption = aqt.mw.addonManager.getConfig(__name__) + return userOption + +def update(_): + global userOption + userOption = None + +mw.addonManager.setConfigUpdatedAction(__name__,update) diff --git a/addons/1566928056/copyNote.py b/addons/1566928056/copyNote.py new file mode 100644 index 00000000000..04acb59eb5f --- /dev/null +++ b/addons/1566928056/copyNote.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- +# Copyright: Arthur Milchior arthur@milchior.fr +# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html +# Select any number of cards in the card browser and create exact copies of each card in the deck +# Feel free to contribute to this code on https://github.com/Arthur-Milchior/anki-copy-note +# Anki's add-on number: 1566928056 + +#This add-ons is heavily based on Kealan Hobelmann's addon 396494452 + +"""To use: + +1) Open the card browser +2) Select the desired notes (at least one card by note) +3) Go to "Edit > Copy Notes in place" or "Edit > Full note copy" + +Both option consider the note you did select, and create a new note with the same content. (Fields and tags) +Both option add the card of the copied note to the deck in which the original card is (this is the main difference with addon 396494452) + +"Copy notes in place" create cards which are new. Empty card's are not copied. +"Full note copy" also copy the reviews paramater (number of reviews, of leeches, easiness, due date...). Empty card's are copied. + +Recall that an «empty cards» is a card that should be deleted by +«check empty card». +""" + +from PyQt5.QtCore import * +from .config import getUserOption +from PyQt5.QtWidgets import * +from PyQt5.QtGui import * +from anki.hooks import addHook +from aqt import mw +from aqt.utils import tooltip, showWarning +from anki.utils import intTime +import anki.notes +#import profile + +def copyNotes(nids,copy_review): + """ + + nids -- id of notes to copy + copy_review -- whether to copy easiness, + """ + mw.checkpoint("Copy Notes") + mw.progress.start() + for nid in nids: + copyNote(nid, copy_review) + # Reset collection and main window + mw.progress.finish() + mw.col.reset() + mw.reset() + + tooltip(_("""Cards copied.""")) + +def copyNote(nid, copy_review): + note = mw.col.getNote(nid) + cards= note.cards() + note.id = timestampID(note.col.db, "notes", note.id if getUserOption().get("Preserve creation time", True) else None) + for card in cards: + card.id = timestampID(note.col.db, "cards", card.id if getUserOption().get("Preserve creation time", True) else None) + if not copy_review: + card.type = 0 + card.ivl = 0 + card.factor = 0 + card.reps = 0 + card.lapses = 0 + card.left = 0 + card.odue = 0 + card.nid = note.id + card.flush() + note.flush() + +def setupMenu(browser): + a = QAction("Note Copy", browser) + a.setShortcut(QKeySequence("Ctrl+C")) # Shortcut for convenience. Added by Didi + a.triggered.connect(lambda : copyNotes(browser.selectedNotes(), copy_review = False)) + browser.form.menuEdit.addSeparator() + browser.form.menuEdit.addAction(a) + a = QAction("Full Notes Copy", browser) + a.setShortcut(QKeySequence("Ctrl+Alt+C")) # Shortcut for convenience. Added by Didi + a.triggered.connect(lambda : copyNotes(browser.selectedNotes(), copy_review = True)) + browser.form.menuEdit.addAction(a) + +addHook("browser.setupMenus", setupMenu) + +def timestampID(db, table, t=None): + "Return a non-conflicting timestamp for table." + # be careful not to create multiple objects without flushing them, or they + # may share an ID. + t = t or intTime(1000) + while db.scalar("select id from %s where id = ?" % table, t): + t += 1 + return t diff --git a/addons/1566928056/meta.json b/addons/1566928056/meta.json new file mode 100644 index 00000000000..ecdde4813b6 --- /dev/null +++ b/addons/1566928056/meta.json @@ -0,0 +1 @@ +{"name": "Copy notes", "mod": 1560116343, "config": {"Preserve creation time": false, "Shortcut: full copy": "Ctrl+Alt+C", "Shortcut: simple copy": "Ctrl+C"}, "disabled": false} \ No newline at end of file diff --git a/addons/1566928056/zipping b/addons/1566928056/zipping new file mode 100644 index 00000000000..1c9c6ecf036 --- /dev/null +++ b/addons/1566928056/zipping @@ -0,0 +1 @@ +zip -r mcne.zip *py zipping *md user_files LICENSE *json --exclude \*~ \ No newline at end of file diff --git a/anki/notes.py b/anki/notes.py index b6ae365ebab..e56f3f2c82f 100644 --- a/anki/notes.py +++ b/anki/notes.py @@ -4,6 +4,7 @@ from anki.utils import fieldChecksum, intTime, \ joinFields, splitFields, stripHTMLMedia, timestampID, guid64 +from copy import copy class Note: @@ -210,6 +211,36 @@ def dupeOrEmpty(self): return 2 return False + # Copy note + ################################################## + def copy(self, copy_review = True, copy_creation = True): + """A copy of the note. + + Save it in the db. + copy_review -- if False, it's similar to a new card. Otherwise, keep every current properties + copy_creation -- whether to keep original creation date. + """ + note = copy(self) + cards= note.cards() + note_date = note.id if copy_creation else None + note.id = timestampID(note.col.db, "notes", t = note_date) + for card in cards: + card_date = card.id if copy_creation else None + card.id = timestampID(note.col.db, "cards", t = note_date) + if not copy_review: + card.type = 0 + card.ivl = 0 + card.factor = 0 + card.reps = 0 + card.lapses = 0 + card.left = 0 + card.odue = 0 + card.nid = note.id + card.flush() + note.flush() + return note + + # Flushing cloze notes ################################################## diff --git a/anki/utils.py b/anki/utils.py index bc52d186c91..934687015a3 100644 --- a/anki/utils.py +++ b/anki/utils.py @@ -217,11 +217,16 @@ def ids2str(ids): """Given a list of integers, return a string '(int1,int2,...)'.""" return "(%s)" % ",".join(str(i) for i in ids) -def timestampID(db, table): - "Return a non-conflicting timestamp for table." +def timestampID(db, table, t=None): + """Return a non-conflicting timestamp for table. + + t -- the time of the id. (It may be changed slightly to ensure + unicity). If none, then the time is now + + """ # be careful not to create multiple objects without flushing them, or they # may share an ID. - t = intTime(1000) + t = t or intTime(1000) while db.scalar("select id from %s where id = ?" % table, t): t += 1 return t diff --git a/aqt/addons.py b/aqt/addons.py index 70275bee3db..59967659064 100644 --- a/aqt/addons.py +++ b/aqt/addons.py @@ -901,6 +901,7 @@ def __hash__(self): """ Set of characteristic of Add-ons incorporated here""" incorporatedAddonsSet = { + Addon("Copy notes", 1566928056, 1560116343, "e12e62094211a8bf06d025ee0f325e8aa4489292", "https://github.com/Arthur-Milchior/anki-copy-note"), } incorporatedAddonsDict = {**{addon.name: addon for addon in incorporatedAddonsSet if addon.name}, diff --git a/aqt/browser.py b/aqt/browser.py index f0e74fef0a0..9569a8d97f4 100644 --- a/aqt/browser.py +++ b/aqt/browser.py @@ -26,6 +26,7 @@ from aqt.webview import AnkiWebView from anki.consts import * from anki.sound import clearAudioQueue, allSounds, play +from anki.notes import Note # Data model @@ -448,6 +449,7 @@ def setupMenus(self): f.actionFindReplace.triggered.connect(self.onFindReplace) f.actionManage_Note_Types.triggered.connect(self.mw.onNoteTypes) f.actionDelete.triggered.connect(self.deleteNotes) + f.actionCopy.triggered.connect(self.actionCopy) # cards f.actionChange_Deck.triggered.connect(self.setDeck) f.action_Info.triggered.connect(self.showCardInfo) @@ -1470,6 +1472,24 @@ def _previewStateAndMod(self): n.load() return (self._previewState, c.id, n.mod) + # Card Copy + ###################################################################### + + def actionCopy(self): + nids = self.selectedNotes() + self.mw.checkpoint("Copy Notes") + copy_review = self.col.conf.get("preserveReviewInfo", True) + copy_creation = self.col.conf.get("preserveCreation", True) + #self.mw.progress.start() + for nid in nids: + note = Note(self.col, id = nid) + note.copy(copy_review = copy_review, copy_creation = copy_creation) + # Reset collection and main window + self.mw.progress.finish() + self.mw.col.reset() + self.mw.reset() + tooltip(_("""Cards copied.""")) + # Card deletion ###################################################################### diff --git a/aqt/preferences.py b/aqt/preferences.py index 32901cce08c..cbcd9ab7d1b 100644 --- a/aqt/preferences.py +++ b/aqt/preferences.py @@ -272,6 +272,8 @@ def updateOneOption(self, name, default=False, check=True, sync=True): storeValue[name] = value extraOptions = [ + ("preserveCreation", True), + ("preserveReviewInfo", True), ] def setupExtra(self): diff --git a/designer/browser.ui b/designer/browser.ui index 71ddbf188e3..8b2b03a43ca 100644 --- a/designer/browser.ui +++ b/designer/browser.ui @@ -236,7 +236,7 @@ 0 0 750 - 22 + 20 @@ -315,6 +315,7 @@ + @@ -599,6 +600,14 @@ Ctrl+K + + + &Copy Note + + + Ctrl+Alt+C + + diff --git a/designer/preferences.ui b/designer/preferences.ui index 0009a7bf405..a643623701c 100644 --- a/designer/preferences.ui +++ b/designer/preferences.ui @@ -489,6 +489,27 @@ + + + + Qt::Horizontal + + + + + + + Note copy: preserve date of creation + + + + + + + Note copy: preserve easyness, interval, due date, ... + + + diff --git a/difference.md b/difference.md index eeaf6f0f9f3..4c80c693ebb 100644 --- a/difference.md +++ b/difference.md @@ -2,3 +2,15 @@ This files list the difference between regular anki and this forked version. It also lists the different options in the Preferences's extra page. +## Copy note (1566928056) +If you select notes in the browser, and do `Notes>Copy Notes` or +`Ctrl+Alt+C`, a copy of the notes are created. + +You have two options in the preferences: +* "Preserve date of creation": keeps the «Created» value in the + browser. It is particularly interesting if you review cards + according to their creation date. +* "Preserve easyness, interval, due date, ...": this create a copy of + each card, as close as possible to the original card. If you uncheck + this, instead, your new cards will be fresh, and you'll start review + from 0. From 1f05a2e3f6244c4a8dd7d03e4d7bf1fa602dbfb8 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Mon, 17 Jun 2019 11:27:39 +0200 Subject: [PATCH 39/68] Keep seen cards --- addons/1402327111/README.md | 34 ++++++++++++++++++++++++++ addons/1402327111/__init__.py | 6 +++++ addons/1402327111/emptyNewCards21.py | 36 ++++++++++++++++++++++++++++ addons/1402327111/meta.json | 1 + addons/1402327111/zipping | 1 + anki/collection.py | 9 ++++--- anki/models.py | 2 +- anki/notes.py | 2 +- aqt/addons.py | 1 + aqt/preferences.py | 1 + designer/preferences.ui | 7 ++++++ difference.md | 8 +++++++ 12 files changed, 103 insertions(+), 5 deletions(-) create mode 100755 addons/1402327111/README.md create mode 100755 addons/1402327111/__init__.py create mode 100755 addons/1402327111/emptyNewCards21.py create mode 100755 addons/1402327111/meta.json create mode 100755 addons/1402327111/zipping diff --git a/addons/1402327111/README.md b/addons/1402327111/README.md new file mode 100755 index 00000000000..638c101828f --- /dev/null +++ b/addons/1402327111/README.md @@ -0,0 +1,34 @@ +# Delete empty cards only if they are new +## Rationale +I do not like to delete cards. If some cards where reviewed and no +longer exists, it is probably a sign that I made something wrong. When +I correct this error, those empty cards will still be here with their +review history. Great ! + +However, I sometime create thoushands of new cards by accident, when I +edit a card type. I want to be able to delete those card. + +The tradeoff I found is deleting cards only when they are new. +If, sometime, I find and empty card, already seen, and it's not a +mistake, I just have to suspend it, and everything is all right. +## Usage +From main window, select "Tools">"Empty new cards". It acts similarly +to "Empty cards". + +## Internal +This add-on redefine anki.collectino._Collection.genCards during the +process. The new method call the previous one. + +The previous method is set back once the action is done. + + +## Links, licence and credits + +Key |Value +------------|------------------------------------------------------------------- +Copyright | Arthur Milchior +Based on | Anki code by Damien Elmes +License | GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +Source in | https://github.com/Arthur-Milchior/anki-empty-new-cards +Addon number| [1402327111](https://ankiweb.net/shared/info/1402327111) +Support me on| [![Ko-fi](https://ko-fi.com/img/Kofi_Logo_Blue.svg)](Ko-fi.com/arthurmilchior) or [![Patreon](http://www.milchior.fr/patreon.png)](https://www.patreon.com/bePatron?u=146206) diff --git a/addons/1402327111/__init__.py b/addons/1402327111/__init__.py new file mode 100755 index 00000000000..eae9088e2ec --- /dev/null +++ b/addons/1402327111/__init__.py @@ -0,0 +1,6 @@ +import sys +if (sys.version_info > (3, 0)): + # Python 3 code in this block + from . import emptyNewCards21 +else: + from . import emptyNewCards20 diff --git a/addons/1402327111/emptyNewCards21.py b/addons/1402327111/emptyNewCards21.py new file mode 100755 index 00000000000..474bf82cf6a --- /dev/null +++ b/addons/1402327111/emptyNewCards21.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Copyright: Arthur Milchior +# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +# feel free to contribute on https://github.com/Arthur-Milchior/anki-empty-new-cards +# Anki add-on number: 1402327111 + +import aqt +from aqt import mw +from aqt.qt import * +from anki.collection import _Collection +from anki.utils import ids2str + + +def check(): + oldGenCards= _Collection.genCards + def genCards(*args,**kwargs): + """Ids of cards which needs to be removed. + + Generate missing cards of a note with id in nids. + """ + col = mw.col + cids = oldGenCards(*args,**kwargs) + cids = ids2str(cids) + toDeleteCids=col.db.list("select id from cards where (type=0 and (id in "+cids+"))") + #l =[] + #for id in toDeleteCids: + #print ("genCards: %s" %str(toDeleteCids)) + return toDeleteCids + _Collection.genCards=genCards + mw.onEmptyCards() + _Collection.genCards=oldGenCards + +action = QAction(aqt.mw) +action.setText("Empty new cards") +mw.form.menuTools.addAction(action) +action.triggered.connect(check) diff --git a/addons/1402327111/meta.json b/addons/1402327111/meta.json new file mode 100755 index 00000000000..ba60303b2df --- /dev/null +++ b/addons/1402327111/meta.json @@ -0,0 +1 @@ +{"name": "Delete empty NEW cards", "mod": 1550534154, "disabled": false} \ No newline at end of file diff --git a/addons/1402327111/zipping b/addons/1402327111/zipping new file mode 100755 index 00000000000..2266a5b0668 --- /dev/null +++ b/addons/1402327111/zipping @@ -0,0 +1 @@ +zip emptyNewCards.zip zipping *py README.md \ No newline at end of file diff --git a/anki/collection.py b/anki/collection.py index cc0f7b94c49..6049c7916ed 100644 --- a/anki/collection.py +++ b/anki/collection.py @@ -531,9 +531,12 @@ def genCards(self, nids): ts += 1 # note any cards that need removing if nid in have: - for ord, id in list(have[nid].items()): - if ord not in avail: - rem.append(id) + for ord, cid in list(have[nid].items()): + if ( + ((not self.conf.get("keepSeenCard", True)) or self.db.scalar(f"select id from cards where id = ? and type = {CARD_NEW}", cid)) + and ord not in avail + ): + rem.append(cid) # bulk update self.db.executemany(""" insert into cards values (?,?,?,?,?,?,0,0,?,0,0,0,0,0,0,0,0,"")""", diff --git a/anki/models.py b/anki/models.py index a20cc66ecbe..43de6431e32 100644 --- a/anki/models.py +++ b/anki/models.py @@ -595,7 +595,7 @@ def _syncTemplates(self, m): """Generate all cards not yet generated, whose note's model is m. It's called only when model is saved, a new model is given and template is asked to be computed""" - rem = self.col.genCards(self.nids(m)) + self.col.genCards(self.nids(m)) # Model changing ########################################################################## diff --git a/anki/notes.py b/anki/notes.py index b6ae365ebab..022f42f076c 100644 --- a/anki/notes.py +++ b/anki/notes.py @@ -225,7 +225,7 @@ def _postFlush(self): Not executed if this note is newlyAdded.""" if not self.newlyAdded: - rem = self.col.genCards([self.id]) + self.col.genCards([self.id]) # popping up a dialog while editing is confusing; instead we can # document that the user should open the templates window to # garbage collect empty cards diff --git a/aqt/addons.py b/aqt/addons.py index 70275bee3db..310c081999b 100644 --- a/aqt/addons.py +++ b/aqt/addons.py @@ -901,6 +901,7 @@ def __hash__(self): """ Set of characteristic of Add-ons incorporated here""" incorporatedAddonsSet = { + Addon("Empty cards returns more usable informations", 25425599, 1560126141, "299a0a7b3092923f5932da0bf8ec90e16db269af", "https://github.com/Arthur-Milchior/anki-clearer-empty-card"), } incorporatedAddonsDict = {**{addon.name: addon for addon in incorporatedAddonsSet if addon.name}, diff --git a/aqt/preferences.py b/aqt/preferences.py index 32901cce08c..b7325528747 100644 --- a/aqt/preferences.py +++ b/aqt/preferences.py @@ -272,6 +272,7 @@ def updateOneOption(self, name, default=False, check=True, sync=True): storeValue[name] = value extraOptions = [ + ("keepSeenCard", True), ] def setupExtra(self): diff --git a/designer/preferences.ui b/designer/preferences.ui index 0009a7bf405..18c7aa761e9 100644 --- a/designer/preferences.ui +++ b/designer/preferences.ui @@ -489,6 +489,13 @@ + + + + Delete empty cards only if they are new + + + diff --git a/difference.md b/difference.md index eeaf6f0f9f3..badba42a2ec 100644 --- a/difference.md +++ b/difference.md @@ -2,3 +2,11 @@ This files list the difference between regular anki and this forked version. It also lists the different options in the Preferences's extra page. +## Keep seen card. +By default, this version of anki does not delete a card if it has +already been seen once. Because, it should probably not be empty, and +so you may want to repair the card type, to create the card again. + +If checked, the option "Delete empty cards only if they are new" set +back «Empty cards» to its original meaning; it'll delete even the seen +cards. From 0dfde9fe122ffd9ed13d0ad3be4ae27992b7b734 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Mon, 17 Jun 2019 22:21:52 +0200 Subject: [PATCH 40/68] Explain deletion --- addons/12287769/LICENSE | 674 +++++++++++++++++++++++++++++++++++ addons/12287769/README.md | 128 +++++++ addons/12287769/__init__.py | 7 + addons/12287769/cards.py | 70 ++++ addons/12287769/debug.py | 3 + addons/12287769/decks.py | 66 ++++ addons/12287769/integrity.py | 131 +++++++ addons/12287769/meta.json | 1 + addons/12287769/models.py | 50 +++ addons/12287769/notes.py | 119 +++++++ addons/12287769/syncer.py | 16 + anki/collection.py | 12 +- anki/decks.py | 16 +- anki/models.py | 9 +- anki/sync.py | 4 +- aqt/addcards.py | 2 +- aqt/addons.py | 1 + aqt/browser.py | 2 +- aqt/main.py | 14 +- aqt/reviewer.py | 2 +- difference.md | 5 + 21 files changed, 1304 insertions(+), 28 deletions(-) create mode 100755 addons/12287769/LICENSE create mode 100755 addons/12287769/README.md create mode 100755 addons/12287769/__init__.py create mode 100755 addons/12287769/cards.py create mode 100755 addons/12287769/debug.py create mode 100755 addons/12287769/decks.py create mode 100755 addons/12287769/integrity.py create mode 100755 addons/12287769/meta.json create mode 100755 addons/12287769/models.py create mode 100755 addons/12287769/notes.py create mode 100755 addons/12287769/syncer.py diff --git a/addons/12287769/LICENSE b/addons/12287769/LICENSE new file mode 100755 index 00000000000..94a9ed024d3 --- /dev/null +++ b/addons/12287769/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/addons/12287769/README.md b/addons/12287769/README.md new file mode 100755 index 00000000000..61254e0f308 --- /dev/null +++ b/addons/12287769/README.md @@ -0,0 +1,128 @@ +# Explaining deletion +## Rationale +Sometime, you have bugs. Card or notes are deleted, and you just don't +know why. You can find the notes in the file deleted.txt, at least if +you are lucky, and that the deletion was clean enough to be logged. + +However, when you want to know why the problem did occur, you miss two +important facts. The time of the deletion. And its reason. + +Furthermore, the same separator (tab) is used for separating the +fields's content, and separating the fields content from the main +column. This does not help readability. + +This add-on logs everything in CSV format, which is non-ambiguous (as +long as you know the note type) +## Usage +### All kinds of deletions +When this add-on is installed, when a note is deleted, it will be +logged in deleted_long.txt instead of deleted.txt. The reason will be +logged. Currently, there may be three kinds of reason: + +You actually asked to delete a whole not. It can appear in the + following cases: +* "Deletion of note {id} requested from the reviewer." While reviewing + card, you pressed on «delete». +* "Deletion of notes {nids} requested from the browser" In the + browser, you pressed on delete. +* "Temporary note" (i.e. you started to create a note, and then decided + you don't want to save it) +* "Remove cards/notes from grave, after sync." This may occur during + synchronization. I'm not sure when it actually occurs. +* "Removing notes {ids} with missing note type" For some reason, you + had a note and anki does not have any idea how to represent + it. Instead of telling you something REALLY wrong occured, and + giving you the data to save it, anki silently delete all of + this. Pretty bad, isn't it ? I sadly have no solution, it would require a + big and complicated add-on to fix it. So my best right now is to at + least let you kow in the deletion file. +* "Note {ids} with no cards" It may occur that a not have no cards + anymore. For example if you empty all of its fields. Or at least, + that there are so little fields used that no cards can be + generated. Once again, instead of letting you correct this error, + anki silently delete the note. You'll be able to find when this + happen thanks to this add-on. At least, if you go in the deleted + file. Sorry that I can't do better. + +You may also decide to delete cards, realize that this lead no card + to some note, and thus deleted the note. It may occur in the + following cases: + +* "Cards {ids} removed because of missing templates." Similar to last + case, but instead of a whole note missing, a single template of a + card is missing. +* "Deleting cards {cids} because we delete the model {m}". A model is + being deleted. You probably was warned that it still had some notes, + and agreed to have it deleted. Thus here it is. +* "Removing card type {template} from model {m}" Similarly to last + case, but this case with card type. +* "Removing cards {cids} because its deck {did} is deleted" Once + again, it's similar, but with deck deletion. +* "Changing notes {nids} from model {oldModel} to {newModel}, leading to deletion of {deleted}" Changing + the note type of a note is essentially creating a new note and + deleting the old one. Kind of. Anyway, it leads to card deletion, + and there it is. + + + +Finally, it may also occur that we have no clue why the deletion +occured. It was probably an add-on request, thus the current add-on + can't control it. This is thus the best error message that I can + offer. +* "Card {ids} removed, no card remained, so note also removed." Anki + was asked to remove cards, but not for any previously listed reason. +* "Removing notes {ids}" Similarly to last case, this just means that + we have no informations more precise to give. +## Warning +It is highly probable that this add-on will be incompatible with other +add-ons. Because it changes a lot of things. Sadly, it seems hard to +do really better. Please let me know whether you find some problem, +with which add-on you have those problem. It means that this add-on +may be better for debugging purpose than for every day use. I'm not +sur. + +In particular it is incompatible with add-ons: +* [Quicker Anki: 802285486](https://ankiweb.net/shared/info/802285486) +* [Database checker/fixer explained, more fixers 1135180054](https://ankiweb.net/shared/info/1135180054) + +Please instead use-addon +[777545149](https://ankiweb.net/shared/info/777545149) which merges +those three add-ons + + +## Internal +This add-on redefine the following methods: +* In `anki.collection`: `_Collection._remNotes`, + `_Collection.fixIntegrity`, `_Collection.remCards`, + `remNotes`, +* In `aqt.AddCards`: `AddCards.removeTempNote`, +* In `anki.sync`: `Syncer.remove`, +* In `anki.models`: `ModelManager.rem`, + `ModelManager.remTemplate`, `_changeCards` +* In `anki.decks`: `DeckManager.rem` +* In `aqt.reviewer`: `Reviewer.onDelet` +* In `aqt.browser`: `Browser._deleteNotes` +* In `aqt.main`: `AnkiQt.BackupThread.onRemNotes` + +## Version 2.0 +It won't be updated. It add only a single more column, with the +reason. And it's less precise. It also only update +`_Collection._remNotes` and the methods calling it, this +`_Collection._remNotes`, i.e. _Collection.remCards, +`_Collection.fixIntegrity`, `Syncer.remove` and +`AddCards.removeTempNote`. + +## Todo +May be allow to use configuration to decide which note to use, which +method to redefine. + +## Links, licence and credits + +Key |Value +------------|------------------------------------------------------------------- +Copyright | Arthur Milchior +Based on | Anki code by Damien Elmes +License | GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +Source in | https://github.com/Arthur-Milchior/anki-note-deletion +Addon number| [12287769](https://ankiweb.net/shared/info/12287769) +Support me on| [![Ko-fi](https://ko-fi.com/img/Kofi_Logo_Blue.svg)](Ko-fi.com/arthurmilchior) or [![Patreon](http://www.milchior.fr/patreon.png)](https://www.patreon.com/bePatron?u=146206) diff --git a/addons/12287769/__init__.py b/addons/12287769/__init__.py new file mode 100755 index 00000000000..a8c57b007ce --- /dev/null +++ b/addons/12287769/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +# Copyright: Arthur Milchior arthur@milchior.fr +# encoding: utf8 +# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html +# Feel free to contribute to this code on https://github.com/Arthur-Milchior/anki-note-deletion +# Add-on number 12287769 https://ankiweb.net/shared/info/12287769 +from . import cards, syncer, notes, models, decks, integrity diff --git a/addons/12287769/cards.py b/addons/12287769/cards.py new file mode 100755 index 00000000000..2986e627df9 --- /dev/null +++ b/addons/12287769/cards.py @@ -0,0 +1,70 @@ +from anki.consts import * +from .debug import debug +from anki.collection import _Collection +from anki.utils import intTime, ids2str +from anki.models import ModelManager + +def remCards(self, ids, notes=True, reason=None): + """Bulk delete cards by ID. + + keyword arguments: + notes -- whether note without cards should be deleted.""" + if not ids: + return + sids = ids2str(ids) + nids = self.db.list("select nid from cards where id in "+sids) + # remove cards + self._logRem(ids, REM_CARD) + self.db.execute("delete from cards where id in "+sids) + # then notes + if not notes: + return + nids = self.db.list(""" +select id from notes where id in %s and id not in (select nid from cards)""" % + ids2str(nids)) + self._remNotes(nids, reason or f"No cards remained for this note.") +_Collection.remCards=remCards + + +def _changeCards(self, nids, oldModel, newModel, map): + """Change the note whose ids are nid to the model newModel, reorder + fields according to map. Write the change in the database + + Remove the cards mapped to nothing + + If the source is a cloze, it is (currently?) mapped to the + card of same order in newModel, independtly of map. + + keyword arguments: + nids -- the list of id of notes to change + oldModel -- the soruce model of the notes + newmodel -- the model of destination of the notes + map -- the dictionnary sending to each card 'ord of the old model a card'ord of the new model or to None + """ + d = [] + deleted = [] + for (cid, ord) in self.col.db.execute( + "select id, ord from cards where nid in "+ids2str(nids)): + # if the src model is a cloze, we ignore the map, as the gui + # doesn't currently support mapping them + if oldModel['type'] == MODEL_CLOZE: + new = ord + if newModel['type'] != MODEL_CLOZE: + # if we're mapping to a regular note, we need to check if + # the destination ord is valid + if len(newModel['tmpls']) <= ord: + new = None + else: + # mapping from a regular note, so the map should be valid + new = map[ord] + if new is not None: + d.append(dict( + cid=cid,new=new,u=self.col.usn(),m=intTime())) + else: + deleted.append(cid) + self.col.db.executemany( + "update cards set ord=:new,usn=:u,mod=:m where id=:cid", + d) + self.col.remCards(deleted,reason=f"Changing notes {nids} from model {oldModel} to {newModel}, leading to deletion of {deleted}") + +ModelManager._changeCards=_changeCards diff --git a/addons/12287769/debug.py b/addons/12287769/debug.py new file mode 100755 index 00000000000..b9c089e4326 --- /dev/null +++ b/addons/12287769/debug.py @@ -0,0 +1,3 @@ +def debug(t): + #print(t) + pass diff --git a/addons/12287769/decks.py b/addons/12287769/decks.py new file mode 100755 index 00000000000..e4299e48e83 --- /dev/null +++ b/addons/12287769/decks.py @@ -0,0 +1,66 @@ +from .debug import debug +from anki.consts import * +from anki.decks import DeckManager +def rem(self, did, cardsToo=False, childrenToo=True): + #difference:simplifying a little bit the code + # adding a reason to remCards + debug("rem") + """Remove the deck whose id is did. + + Does not delete the default deck, but rename it. + + Log the removal, even if the deck does not exists, assuming it + is not default. + + Keyword arguments: + cardsToo -- if set to true, delete its card. + ChildrenToo -- if set to false, + """ + deck = self.get(did)#new + dname = deck.get('name')# new + if str(did) == '1': + # we won't allow the default deck to be deleted, but if it's a + # child of an existing deck then it needs to be renamed + if '::' in dname:#changed: used dname instead of deck['name'] + base = dname.split("::")[-1]#changed: used dname instead of deck['name'] + suffix = "" + while True: + # find an unused name + name = base + suffix + if not self.byName(name): + dname = name + self.save(deck) + break + suffix += "1" + return + # log the removal regardless of whether we have the deck or not + self.col._logRem([did], REM_DECK) + # do nothing else if doesn't exist + if deck is None:# simplifying the condition since deck was already found + return + if deck['dyn']: + # deleting a cramming deck returns cards to their previous deck + # rather than deleting the cards + self.col.sched.emptyDyn(did) + if childrenToo: + for name, id in self.children(did): + self.rem(id, cardsToo) + else: + # delete children first + if childrenToo: + # we don't want to delete children when syncing + for name, id in self.children(did): + self.rem(id, cardsToo) + # delete cards too? + if cardsToo: + # don't use cids(), as we want cards in cram decks too + cids = self.col.db.list( + "select id from cards where did=? or odid=?", did, did) + self.col.remCards(cids,reason=f"The last card of this note was in deck {did}:{dname}, which got deleted.") # adding reason + # delete the deck and add a grave (it seems no grave is added) + del self.decks[str(did)] + # ensure we have an active deck. + if did in self.active(): + self.select(int(list(self.decks.keys())[0])) + self.save() +DeckManager.rem=rem diff --git a/addons/12287769/integrity.py b/addons/12287769/integrity.py new file mode 100755 index 00000000000..9705c5e4b33 --- /dev/null +++ b/addons/12287769/integrity.py @@ -0,0 +1,131 @@ +from .debug import debug +from anki.collection import _Collection +import os +from anki.utils import intTime, ids2str +from anki.consts import * + + +def fixIntegrity(self): + debug("fixIntegrity") + "Fix possible problems and rebuild caches." + problems = [] + self.save() + oldSize = os.stat(self.path)[stat.ST_SIZE] + if self.db.scalar("pragma integrity_check") != "ok": + return (_("Collection is corrupt. Please see the manual."), False) + # note types with a missing model + ids = self.db.list(""" +select id from notes where mid not in """ + ids2str(self.models.ids())) + if ids: + problems.append( + ngettext("Deleted %d note with missing note type.", + "Deleted %d notes with missing note type.", len(ids)) + % len(ids)) + self.remNotes(ids,reason=f"Removing notes {ids} with missing note type") + # for each model + for m in self.models.all(): + for t in m['tmpls']: + if t['did'] == "None": + t['did'] = None + problems.append(_("Fixed AnkiDroid deck override bug.")) + self.models.save(m) + if m['type'] == MODEL_STD: + # model with missing req specification + if 'req' not in m: + self.models._updateRequired(m) + problems.append(_("Fixed note type: %s") % m['name']) + # cards with invalid ordinal + ids = self.db.list(""" +select id from cards where ord not in %s and nid in ( +select id from notes where mid = ?)""" % + ids2str([t['ord'] for t in m['tmpls']]), + m['id']) + if ids: + problems.append( + ngettext("Deleted %d card with missing template.", + "Deleted %d cards with missing template.", + len(ids)) % len(ids)) + self.remCards(ids, reason=f"Cards {ids} removed because of missing templates.") + # notes with invalid field count + ids = [] + for id, flds in self.db.execute( + "select id, flds from notes where mid = ?", m['id']): + if (flds.count("\x1f") + 1) != len(m['flds']): + ids.append(id) + if ids: + problems.append( + ngettext("Deleted %d note with wrong field count.", + "Deleted %d notes with wrong field count.", + len(ids)) % len(ids)) + self.remNotes(ids,f"Deleted notes {ids} with wrong field count") + # delete any notes with missing cards + ids = self.db.list(""" +select id from notes where id not in (select distinct nid from cards)""") + if ids: + cnt = len(ids) + problems.append( + ngettext("Deleted %d note with no cards.", + "Deleted %d notes with no cards.", cnt) % cnt) + self._remNotes(ids,reason=f"Note {ids} with no cards") + # cards with missing notes + ids = self.db.list(""" +select id from cards where nid not in (select id from notes)""") + if ids: + cnt = len(ids) + problems.append( + ngettext("Deleted %d card with missing note.", + "Deleted %d cards with missing note.", cnt) % cnt) + self.remCards(ids, reason="Cards {ids} removed because of missing notes.") + # cards with odue set when it shouldn't be + ids = self.db.list(""" +select id from cards where odue > 0 and (type=1 or queue=2) and not odid""") + if ids: + cnt = len(ids) + problems.append( + ngettext("Fixed %d card with invalid properties.", + "Fixed %d cards with invalid properties.", cnt) % cnt) + self.db.execute("update cards set odue=0 where id in "+ + ids2str(ids)) + # cards with odid set when not in a dyn deck + dids = [id for id in self.decks.allIds() if not self.decks.isDyn(id)] + ids = self.db.list(""" +select id from cards where odid > 0 and did in %s""" % ids2str(dids)) + if ids: + cnt = len(ids) + problems.append( + ngettext("Fixed %d card with invalid properties.", + "Fixed %d cards with invalid properties.", cnt) % cnt) + self.db.execute("update cards set odid=0, odue=0 where id in "+ + ids2str(ids)) + # tags + self.tags.registerNotes() + # field cache + for m in self.models.all(): + self.updateFieldCache(self.models.nids(m)) + # new cards can't have a due position > 32 bits + self.db.execute(""" +update cards set due = 1000000, mod = ?, usn = ? where due > 1000000 +and type = 0""", intTime(), self.usn()) + # new card position + self.conf['nextPos'] = self.db.scalar( + "select max(due)+1 from cards where type = 0") or 0 + # reviews should have a reasonable due # + ids = self.db.list( + "select id from cards where queue = 2 and due > 100000") + if ids: + problems.append("Reviews had incorrect due date.") + self.db.execute( + "update cards set due = ?, ivl = 1, mod = ?, usn = ? where id in %s" + % ids2str(ids), self.sched.today, intTime(), self.usn()) + # and finally, optimize + self.optimize() + newSize = os.stat(self.path)[stat.ST_SIZE] + txt = _("Database rebuilt and optimized.") + ok = not problems + problems.append(txt) + # if any problems were found, force a full sync + if not ok: + self.modSchema(check=False) + self.save() + return ("\n".join(problems), ok) +_Collection.fixIntegrity=fixIntegrity diff --git a/addons/12287769/meta.json b/addons/12287769/meta.json new file mode 100755 index 00000000000..f9c57ef230e --- /dev/null +++ b/addons/12287769/meta.json @@ -0,0 +1 @@ +{"name": "Explain deletions", "mod": 1556149013} \ No newline at end of file diff --git a/addons/12287769/models.py b/addons/12287769/models.py new file mode 100755 index 00000000000..0f7a6bb41d3 --- /dev/null +++ b/addons/12287769/models.py @@ -0,0 +1,50 @@ +from anki.models import * +from anki.utils import intTime + + +def rem(self, m): + "Delete model, and all its cards/notes." + self.col.modSchema(check=True) + current = self.current()['id'] == m['id'] + # delete notes/cards + cids=self.col.db.list(""" +select id from cards where nid in (select id from notes where mid = ?)""", + m['id']) + self.col.remCards(cids, reason=f"Deleting cards {cids} because we delete the model {m}") + # then the model + del self.models[str(m['id'])] + self.save() + # GUI should ensure last model is not deleted + if current: + self.setCurrent(list(self.models.values())[0]) +ModelManager.rem = rem +def remTemplate(self, m, template): + "False if removing template would leave orphan notes." + assert len(m['tmpls']) > 1 + # find cards using this template + ord = m['tmpls'].index(template) + cids = self.col.db.list(""" +select c.id from cards c, notes f where c.nid=f.id and mid = ? and ord = ?""", + m['id'], ord) + # all notes with this template must have at least two cards, or we + # could end up creating orphaned notes + if self.col.db.scalar(""" +select nid, count() from cards where +nid in (select nid from cards where id in %s) +group by nid +having count() < 2 +limit 1""" % ids2str(cids)): + return False + # ok to proceed; remove cards + self.col.modSchema(check=True) + self.col.remCards(cids,reason=f"Removing card type {template} from model {m}") + # shift ordinals + self.col.db.execute(""" +update cards set ord = ord - 1, usn = ?, mod = ? + where nid in (select id from notes where mid = ?) and ord > ?""", + self.col.usn(), intTime(), m['id'], ord) + m['tmpls'].remove(template) + self._updateTemplOrds(m) + self.save(m) + return True +ModelManager.remTemplate=remTemplate diff --git a/addons/12287769/notes.py b/addons/12287769/notes.py new file mode 100755 index 00000000000..d334e96ea4e --- /dev/null +++ b/addons/12287769/notes.py @@ -0,0 +1,119 @@ +import csv +from .debug import debug +from aqt.addcards import AddCards +from aqt.utils import tooltip +from aqt import mw +import datetime +from anki.collection import _Collection +import os +from anki.utils import intTime, ids2str, splitFields +from anki.consts import * + +def _remNotes(self, ids, reason=""): + """Bulk delete notes by ID. Don't call this directly. + + keyword arguments: + self -- collection""" + #only difference: adding a reason for deletion, calling onRemNotes + if not ids: + return + strids = ids2str(ids) + # we need to log these independently of cards, as one side may have + # more card templates + mw.onRemNotes(self,ids,reason=reason)# new + self._logRem(ids, REM_NOTE) + self.db.execute("delete from notes where id in %s" % strids) + +_Collection._remNotes=_remNotes + +def removeTempNote(self, note): + #Only difference: adding a reason for deletion (normally it should not be logged anyway) + debug("removeTempNote") + if not note or not note.id: + return + # we don't have to worry about cards; just the note + self.mw.col._remNotes([note.id],reason="Temporary note") +AddCards.removeTempNote=removeTempNote + +def remNotes(self, ids, reason=None): + #only diff: adding a reason for deletion + debug("remNotes") + """Removes all cards associated to the notes whose id is in ids""" + self.remCards(self.db.list("select id from cards where nid in "+ + ids2str(ids)), reason=reason or f"Removing notes {ids}") +_Collection.remNotes=remNotes + +from aqt.reviewer import Reviewer +def onDelete(self): + # only diff: adding a reason for deletion + debug("onDelete") + # need to check state because the shortcut is global to the main + # window + if self.mw.state != "review" or not self.card: + return + self.mw.checkpoint(_("Delete")) + cnt = len(self.card.note().cards()) + id = self.card.note().id + self.mw.col.remNotes([id],reason=f"Deletion of note {id} requested from the reviewer.") + self.mw.reset() + tooltip(ngettext( + "Note and its %d card deleted.", + "Note and its %d cards deleted.", + cnt) % cnt) +Reviewer.onDelete=onDelete + +from aqt.browser import Browser +def _deleteNotes(self): + # only diff: adding reason + nids = self.selectedNotes() + if not nids: + return + self.mw.checkpoint(_("Delete Notes")) + self.model.beginReset() + # figure out where to place the cursor after the deletion + curRow = self.form.tableView.selectionModel().currentIndex().row() + selectedRows = [i.row() for i in + self.form.tableView.selectionModel().selectedRows()] + if min(selectedRows) < curRow < max(selectedRows): + # last selection in middle; place one below last selected item + move = sum(1 for i in selectedRows if i > curRow) + newRow = curRow - move + elif max(selectedRows) <= curRow: + # last selection at bottom; place one below bottommost selection + newRow = max(selectedRows) - len(nids) + 1 + else: + # last selection at top; place one above topmost selection + newRow = min(selectedRows) - 1 + self.col.remNotes(nids, reason=f"Deletion of notes {nids} requested from the browser") + self.search() + if len(self.model.cards): + newRow = min(newRow, len(self.model.cards) - 1) + newRow = max(newRow, 0) + self.model.focusedCard = self.model.cards[newRow] + self.model.endReset() + self.mw.requireReset() + tooltip(ngettext("%d note deleted.", "%d notes deleted.", len(nids)) % len(nids)) + +Browser._deleteNotes = _deleteNotes + +from aqt.main import AnkiQt +def onRemNotes(self, col, nids, reason=""): + debug("onRemNotes") + """Append (reason,deletion time id, deletion time readable, id, model id, fields) to the end of deleted_long.txt + + This is done for each id of nids. + This method is added to the hook remNotes; and executed on note deletion. + """ + path = os.path.join(self.pm.profileFolder(), "deleted_long.txt") + existed = os.path.exists(path) + with open(path, "a") as f: + if not existed: + f.write(b"reason\tdeletion time id\thuman deletion time\tid\tmid\tfields\t\n") + for id, mid, flds in col.db.execute( + "select id, mid, flds from notes where id in %s" % + ids2str(nids)): + fields = splitFields(flds) + writer.writerow([reason,str(intTime()),str(datetime.datetime.now()),str(id), str(mid)]+fields) + + +AnkiQt.onRemNotes = onRemNotes diff --git a/addons/12287769/syncer.py b/addons/12287769/syncer.py new file mode 100755 index 00000000000..c50d143d3db --- /dev/null +++ b/addons/12287769/syncer.py @@ -0,0 +1,16 @@ +from .debug import debug +from anki.sync import Syncer +def remove(self, graves): + # pretend to be the server so we don't set usn = -1 + self.col.server = True + + # notes first, so we don't end up with duplicate graves + self.col._remNotes(graves['notes'],reason=f"Remove notes {graves['notes']} from grave after sync") + # then cards + self.col.remCards(graves['cards'], notes=False, reason=f"Remove cards {graves['cards']} from grave, after sync.") + # and decks + for oid in graves['decks']: + self.col.decks.rem(oid, childrenToo=False) + + self.col.server = False +Syncer.remove=remove diff --git a/anki/collection.py b/anki/collection.py index cc0f7b94c49..209c9ed066a 100644 --- a/anki/collection.py +++ b/anki/collection.py @@ -427,19 +427,19 @@ def addNote(self, note): ncards += 1 return ncards - def remNotes(self, ids): + def remNotes(self, ids, reason = ""): """Removes all cards associated to the notes whose id is in ids""" self.remCards(self.db.list("select id from cards where nid in "+ - ids2str(ids))) + ids2str(ids)), reason=reason or f"Removing notes {ids}") - def _remNotes(self, ids): + def _remNotes(self, ids, reason=""): "Bulk delete notes by ID. Don't call this directly." if not ids: return strids = ids2str(ids) # we need to log these independently of cards, as one side may have # more card templates - runHook("remNotes", self, ids) + runHook("remNotes", self, ids, reason) self._logRem(ids, REM_NOTE) self.db.execute("delete from notes where id in %s" % strids) @@ -628,7 +628,7 @@ def isEmpty(self): def cardCount(self): return self.db.scalar("select count() from cards") - def remCards(self, ids, notes=True): + def remCards(self, ids, notes=True, reason=None): """Bulk delete cards by ID. keyword arguments: @@ -646,7 +646,7 @@ def remCards(self, ids, notes=True): nids = self.db.list(""" select id from notes where id in %s and id not in (select nid from cards)""" % ids2str(nids)) - self._remNotes(nids) + self._remNotes(nids, reason or f"No cards remained for this note.") def emptyCids(self): """The card id of empty cards of the collection""" diff --git a/anki/decks.py b/anki/decks.py index f7b679d39c1..c636d0b6526 100644 --- a/anki/decks.py +++ b/anki/decks.py @@ -272,6 +272,8 @@ def id(self, name, create=True, type=None): return int(id) def rem(self, did, cardsToo=False, childrenToo=True): + #difference:simplifying a little bit the code + # adding a reason to remCards """Remove the deck whose id is did. Does not delete the default deck, but rename it. @@ -283,18 +285,19 @@ def rem(self, did, cardsToo=False, childrenToo=True): cardsToo -- if set to true, delete its card. ChildrenToo -- if set to false, """ + deck = self.get(did, default=False)#new + dname = deck.get('name')# new if str(did) == '1': # we won't allow the default deck to be deleted, but if it's a # child of an existing deck then it needs to be renamed - deck = self.get(did) - if '::' in deck['name']: - base = deck['name'].split("::")[-1] + if '::' in dname:#changed: used dname instead of deck['name'] + base = dname.split("::")[-1]#changed: used dname instead of deck['name'] suffix = "" while True: # find an unused name name = base + suffix if not self.byName(name): - deck['name'] = name + dname = name self.save(deck) break suffix += "1" @@ -302,9 +305,8 @@ def rem(self, did, cardsToo=False, childrenToo=True): # log the removal regardless of whether we have the deck or not self.col._logRem([did], REM_DECK) # do nothing else if doesn't exist - if not str(did) in self.decks: + if deck is None:# simplifying the condition since deck was already found return - deck = self.get(did) if deck['dyn']: # deleting a cramming deck returns cards to their previous deck # rather than deleting the cards @@ -323,7 +325,7 @@ def rem(self, did, cardsToo=False, childrenToo=True): # don't use cids(), as we want cards in cram decks too cids = self.col.db.list( "select id from cards where did=? or odid=?", did, did) - self.col.remCards(cids) + self.col.remCards(cids,reason=f"The last card of this note was in deck {did}:{dname}, which got deleted.") # adding reason # delete the deck and add a grave (it seems no grave is added) del self.decks[str(did)] # ensure we have an active deck. diff --git a/anki/models.py b/anki/models.py index a20cc66ecbe..90ed8ae7cc5 100644 --- a/anki/models.py +++ b/anki/models.py @@ -240,9 +240,10 @@ def rem(self, m): self.col.modSchema(check=True) current = self.current()['id'] == m['id'] # delete notes/cards - self.col.remCards(self.col.db.list(""" + cids = self.col.db.list(""" select id from cards where nid in (select id from notes where mid = ?)""", - m['id'])) + m['id']) + self.col.remCards(cids, reason=f"Deleting cards {cids} because we delete the model {m}") # then the model del self.models[str(m['id'])] self.save() @@ -550,7 +551,7 @@ def remTemplate(self, m, template): return False # ok to proceed; remove cards self.col.modSchema(check=True) - self.col.remCards(cids) + self.col.remCards(cids, reason=f"Removing card type {template} from model {m}") # shift ordinals self.col.db.execute(""" update cards set ord = ord - 1, usn = ?, mod = ? @@ -690,7 +691,7 @@ def _changeCards(self, nids, oldModel, newModel, map): self.col.db.executemany( "update cards set ord=:new,usn=:u,mod=:m where id=:cid", d) - self.col.remCards(deleted) + self.col.remCards(deleted,reason=f"Changing notes {nids} from model {oldModel} to {newModel}, leading to deletion of {deleted}") # Schema hash ########################################################################## diff --git a/anki/sync.py b/anki/sync.py index 590afc6c560..a9c07000981 100644 --- a/anki/sync.py +++ b/anki/sync.py @@ -404,9 +404,9 @@ def remove(self, graves): self.col.server = True # notes first, so we don't end up with duplicate graves - self.col._remNotes(graves['notes']) + self.col._remNotes(graves['notes'], reason=f"Remove notes {graves['notes']} from grave after sync") # then cards - self.col.remCards(graves['cards'], notes=False) + self.col.remCards(graves['cards'], notes=False, reason=f"Remove cards {graves['cards']} from grave, after sync.") # and decks for oid in graves['decks']: self.col.decks.rem(oid, childrenToo=False) diff --git a/aqt/addcards.py b/aqt/addcards.py index 19c00d83fcb..b0ec58616ed 100644 --- a/aqt/addcards.py +++ b/aqt/addcards.py @@ -139,7 +139,7 @@ def removeTempNote(self, note): if not note or not note.id: return # we don't have to worry about cards; just the note - self.mw.col._remNotes([note.id]) + self.mw.col._remNotes([note.id], reason="Temporary note") def addHistory(self, note): self.history.insert(0, note.id) diff --git a/aqt/addons.py b/aqt/addons.py index 70275bee3db..dd21e2f733d 100644 --- a/aqt/addons.py +++ b/aqt/addons.py @@ -901,6 +901,7 @@ def __hash__(self): """ Set of characteristic of Add-ons incorporated here""" incorporatedAddonsSet = { + Addon("Explain deletions", 12287769, 1556149013, "aa0d9485974fafd109ccd426f393a0d17aa94306", "https://github.com/Arthur-Milchior/anki-note-deletion"), } incorporatedAddonsDict = {**{addon.name: addon for addon in incorporatedAddonsSet if addon.name}, diff --git a/aqt/browser.py b/aqt/browser.py index f0e74fef0a0..9d882ebcfaa 100644 --- a/aqt/browser.py +++ b/aqt/browser.py @@ -1499,7 +1499,7 @@ def _deleteNotes(self): else: # last selection at top; place one above topmost selection newRow = min(selectedRows) - 1 - self.col.remNotes(nids) + self.col.remNotes(nids, reason=f"Deletion of notes {nids} requested from the browser") self.search() if len(self.model.cards): newRow = min(newRow, len(self.model.cards) - 1) diff --git a/aqt/main.py b/aqt/main.py index 4525ba78078..8ade8803593 100644 --- a/aqt/main.py +++ b/aqt/main.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +import datetime import re import signal import zipfile @@ -25,6 +26,7 @@ import aqt.mediasrv import anki.sound import anki.mpv +import csv from aqt.utils import saveGeom, restoreGeom, showInfo, showWarning, \ restoreState, getOnlyText, askUser, showText, tooltip, \ openHelp, openLink, checkInvalidFilename, getFile @@ -1119,23 +1121,23 @@ def onMpvIdle(self): # Log note deletion ########################################################################## - def onRemNotes(self, col, nids): + def onRemNotes(self, col, nids, reason=""): """Append (id, model id and fields) to the end of deleted.txt This is done for each id of nids. This method is added to the hook remNotes; and executed on note deletion. """ - path = os.path.join(self.pm.profileFolder(), "deleted.txt") + path = os.path.join(self.pm.profileFolder(), "deleted_long.txt") existed = os.path.exists(path) - with open(path, "ab") as f: + with open(path, "a", newline = '') as f: + writer = csv.writer(f) if not existed: - f.write(b"nid\tmid\tfields\n") + f.write("reason\tdeletion time id\thuman deletion time\tid\tmid\tfields\t\n")#difference: more fields for id, mid, flds in col.db.execute( "select id, mid, flds from notes where id in %s" % ids2str(nids)): fields = splitFields(flds) - f.write(("\t".join([str(id), str(mid)] + fields)).encode("utf8")) - f.write(b"\n") + writer.writerow([reason,str(intTime()),str(datetime.datetime.now()),str(id), str(mid)]+fields) # Schema modifications ########################################################################## diff --git a/aqt/reviewer.py b/aqt/reviewer.py index db9ce2129fc..31cc829dd6c 100644 --- a/aqt/reviewer.py +++ b/aqt/reviewer.py @@ -687,7 +687,7 @@ def onDelete(self): return self.mw.checkpoint(_("Delete")) cnt = len(self.card.note().cards()) - self.mw.col.remNotes([self.card.note().id]) + self.mw.col.remNotes([self.card.note().id], reason=f"Deletion of note {id} requested from the reviewer.") self.mw.reset() tooltip(ngettext( "Note and its %d card deleted.", diff --git a/difference.md b/difference.md index eeaf6f0f9f3..b8b89c3d7d3 100644 --- a/difference.md +++ b/difference.md @@ -2,3 +2,8 @@ This files list the difference between regular anki and this forked version. It also lists the different options in the Preferences's extra page. +## Explain deletion (12287769) +In your collection's folder, the file `deleted_long.txt` contains a +list of every notes deleted, the reason of the deletion, and some +other informations. It's saved as CSV, so that it can be recovered +(with difficulty, but it theoretically can) From 7a52401b44b25033cd30a04b06f0db81d03226c6 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Mon, 17 Jun 2019 13:02:40 +0200 Subject: [PATCH 41/68] Export cards selected in the browser --- addons/1983204951/LICENSE | 674 +++++++++++++++++++++++++++++ addons/1983204951/README.md | 70 +++ addons/1983204951/__init__.py | 8 + addons/1983204951/ankiExporting.py | 60 +++ addons/1983204951/aqtExporting.py | 157 +++++++ addons/1983204951/browser.py | 12 + addons/1983204951/config.json | 8 + addons/1983204951/config.md | 13 + addons/1983204951/meta.json | 1 + addons/1983204951/zipping | 2 + anki/cards.py | 7 +- anki/exporting.py | 9 +- aqt/addons.py | 1 + aqt/browser.py | 2 + aqt/exporting.py | 31 +- aqt/preferences.py | 1 + designer/browser.ui | 11 +- designer/preferences.ui | 7 + difference.md | 15 + 19 files changed, 1078 insertions(+), 11 deletions(-) create mode 100644 addons/1983204951/LICENSE create mode 100644 addons/1983204951/README.md create mode 100644 addons/1983204951/__init__.py create mode 100644 addons/1983204951/ankiExporting.py create mode 100644 addons/1983204951/aqtExporting.py create mode 100644 addons/1983204951/browser.py create mode 100644 addons/1983204951/config.json create mode 100644 addons/1983204951/config.md create mode 100644 addons/1983204951/meta.json create mode 100755 addons/1983204951/zipping diff --git a/addons/1983204951/LICENSE b/addons/1983204951/LICENSE new file mode 100644 index 00000000000..f288702d2fa --- /dev/null +++ b/addons/1983204951/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/addons/1983204951/README.md b/addons/1983204951/README.md new file mode 100644 index 00000000000..9a82a49f6ae --- /dev/null +++ b/addons/1983204951/README.md @@ -0,0 +1,70 @@ +# Export cards selected in the Browser +## Rationale +Sometime, I want to export only some notes. And the cards are not all +in the same deck. I wanted to be able to just select cards in the +browser and export them. Thus, this add-on is here. + +Another problem with anki's exporting system is that it exports cards, +and not notes. Image that you have a card in a deck, and its sibling +(other card of the same note) in another deck. And that you export the +deck of the first card. Only the first card will be exported. Now, if +you import the exported package, Anki will eventually detect that some +siblings are missing, and will generate those as new cards. Thus the +entire review history of the siblings will be missing. Personnally, I +prefer to export siblings, even if it means exporting more than a +deck. But since people may disagree, this option is configurable. + +## Usage +Open the Browser, select the cards you want to export. Then go to +Edit>Export selection (Ctrl+Shift+E), and the standard exporting +window will open, but with a new options: "Browser's +selection". Keeping this option, you can export your selection as you +exported decks. + +## Configuration +You can ignore this section. Unless you really want +to export cards without their siblings in package/exporters installed +by other add-ons, default should be okay. + +"siblings": A mapping stating, for each kind of export option, whether you want that cards are exported with their siblings (other cards from the same note). +* null means that you use the "default" setting. +* true means that, when you export a card, you export their siblings. +* false means that you can export cards without their siblings. + +The entry of the "siblings" mapping are keys of exporter, or the constant "default". To control exporter created by other add-on, check their code to find their "key" value, and add it to this mapping. The default values are: +* "default": the default value, when no other are used. +* "Notes in Plain Text": exporting notes as txt files. +* "Cards in Plain Text": exporting cards as txt files. +* "Anki Deck Package": exporting a .apkg file +If another exporter is added by an add-on, add its key to this mapping + +"Anki 2.0 Deck" is not listed. Indeed, when the entire collection is exported, this parameter becomes useless. + +## Internal +This add-on change +* in `aqt.exporting`, the method `ExportDialog.__init__`. The new + method calls the previous version of the method. +* in `aqt.exporting`, the methods `ExportDialog.setup`, + `ExportDialog.accept`. The new methods do not call the previous + version of the method. +* in `anki.exporting`, the method `Exporter.cardIds` is + modified. The former version of the method is called if the exporter + is called from the browser with a non-empty selection. +* in `anki.exporting`, the method `AnkiExporter.exportInto` is + redefined, not calling the previous version. A single line is + changed. This change is sent to Anki in a (pull + request)[https://github.com/dae/anki/pull/263]. + +## Version 2.0 +Sorry, but no such version, unless someone wants to pay for it. + +## Links, licence and credits + +Key |Value +------------|------------------------------------------------------------------- +Copyright | Arthur Milchior +Based on | Anki code by Damien Elmes +License | GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +Source in | https://github.com/Arthur-Milchior/anki-export-from-browser +Addon number| [1983204951](https://ankiweb.net/shared/info/1983204951) +Support me on| [![Ko-fi](https://ko-fi.com/img/Kofi_Logo_Blue.svg)](Ko-fi.com/arthurmilchior) or [![Patreon](http://www.milchior.fr/patreon.png)](https://www.patreon.com/bePatron?u=146206) diff --git a/addons/1983204951/__init__.py b/addons/1983204951/__init__.py new file mode 100644 index 00000000000..928ffd173f2 --- /dev/null +++ b/addons/1983204951/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +# Copyright: Arthur Milchior arthur@milchior.fr +# encoding: utf8 +# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html +# Feel free to contribute to this code on https://github.com/Arthur-Milchior/anki-export-from-browser +# Add-on number 1983204951 https://ankiweb.net/shared/info/1983204951 + +from . import ankiExporting, aqtExporting, browser diff --git a/addons/1983204951/ankiExporting.py b/addons/1983204951/ankiExporting.py new file mode 100644 index 00000000000..131b9d03130 --- /dev/null +++ b/addons/1983204951/ankiExporting.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +# Copyright: Arthur Milchior arthur@milchior.fr +# encoding: utf8 +# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html +# Feel free to contribute to this code on https://github.com/Arthur-Milchior/anki-export-from-browser +# Add-on number 1983204951 https://ankiweb.net/shared/info/1983204951 + +import re +import os +import time +from PyQt5.QtWidgets import QAction + +from aqt import mw +from aqt.exporting import ExportDialog +from aqt.qt import QKeySequence, QPushButton, QDialogButtonBox, QDialog +from aqt.utils import tooltip, showInfo, getSaveFile, checkInvalidFilename, showWarning + +from anki import Collection +from anki.exporting import Exporter, AnkiExporter, exporters +from anki.hooks import addHook, remHook +from anki.lang import ngettext +from anki.utils import ids2str +from anki.lang import _ + + +def debug(t): + print(t) + pass + +def needSiblings(self): + userOption = mw.addonManager.getConfig(__name__) + siblings = userOption["siblings"] + r=siblings.get(self.key) + if r is None: + r= siblings.get("default") + if r is None: + r = True + debug(f"Calling needSiblings() with key {self.key}, returning {r}") + return r +Exporter.needSiblings = needSiblings + +def siblings(cids): + siblings = mw.col.db.list(f"select id from cards where nid in (select nid from cards where id in {ids2str(cids)})") + debug(f"Calling siblings({cids}), returning {siblings}") + return siblings + +oldCardIds= Exporter.cardIds +def cardIds(self): + debug(f"Calling cardIds()") + if self.cids is not None: + debug(f"cids: {self.cids}, returning it") + cids= self.cids + self.count = len(cids) + else: + cids=oldCardIds(self) + debug(f"cids: {self.did}, returning {cids}") + if self.needSiblings(): + cids = siblings(cids) + return cids +Exporter.cardIds = cardIds diff --git a/addons/1983204951/aqtExporting.py b/addons/1983204951/aqtExporting.py new file mode 100644 index 00000000000..0765aee6340 --- /dev/null +++ b/addons/1983204951/aqtExporting.py @@ -0,0 +1,157 @@ +# -*- coding: utf-8 -*- +# Copyright: Arthur Milchior arthur@milchior.fr +# encoding: utf8 +# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html +# Feel free to contribute to this code on https://github.com/Arthur-Milchior/anki-export-from-browser +# Add-on number 1983204951 https://ankiweb.net/shared/info/1983204951 + +import re +import os +import time +from PyQt5.QtWidgets import QAction + +from aqt import mw +from aqt.exporting import ExportDialog +from aqt.qt import QKeySequence, QPushButton, QDialogButtonBox, QDialog +from aqt.utils import tooltip, showInfo, getSaveFile, checkInvalidFilename, showWarning + +from anki import Collection +from anki.exporting import Exporter, AnkiExporter, exporters +from anki.hooks import addHook, remHook +from anki.lang import ngettext +from anki.utils import ids2str +from anki.lang import _ + + +def debug(t): + print(t) + pass + +#Change to ExportDialog +oldInit=ExportDialog.__init__ +def __init__(self, mw, did=None, cids=None): + self.cids = cids + oldInit(self,mw,did=did) +ExportDialog.__init__=__init__ + +# oldExporterChanged = ExportDialog.exportChanged +# def exporterChanged(self, idx): +# oldExporterChanged(self,idx) +# ExportDialog.exportChanged=exporterChanged + + +def setup(self, did): + """ + keyword arguments: + did -- if None, then export whole anki. If did, export this deck. If list of cids, expots those cids. + """ + debug(f"Calling setup({did}), its cids is {self.cids}") + self.exporters = exporters() + # if a deck specified, start with .apkg type selected + idx = 0 + if did or self.cids: + for c, (k,e) in enumerate(self.exporters): + if e.ext == ".apkg": + idx = c + break + self.frm.format.insertItems(0, [e[0] for e in self.exporters]) + self.frm.format.setCurrentIndex(idx) + self.frm.format.activated.connect(self.exporterChanged) + self.exporterChanged(idx) + # deck list + self.decks = [_("All Decks")] + if self.cids: + bs=_("Browser's selection") + debug(f"Adding {bs}") + self.decks = self.decks+[bs] + self.decks = self.decks + sorted(self.col.decks.allNames()) + self.frm.deck.addItems(self.decks) + # save button + b = QPushButton(_("Export...")) + self.frm.buttonBox.addButton(b, QDialogButtonBox.AcceptRole) + # set default option if accessed through deck button + if did: + name = self.mw.col.decks.get(did)['name'] + index = self.frm.deck.findText(name) + self.frm.deck.setCurrentIndex(index) + if self.cids: + self.frm.deck.setCurrentIndex(1) +ExportDialog.setup=setup + +def accept(self): + debug(f"Calling accept()") + self.exporter.includeSched = ( + self.frm.includeSched.isChecked()) + self.exporter.includeMedia = ( + self.frm.includeMedia.isChecked()) + self.exporter.includeTags = ( + self.frm.includeTags.isChecked()) + self.exporter.includeHTML = ( + self.frm.includeHTML.isChecked()) +### Starting change + if self.frm.deck.currentIndex() == 0:#position 0 means: all decks. + self.exporter.did = None + self.exporter.cids = None + elif self.frm.deck.currentIndex() == 1 and self.cids is not None:#position 1 means: selected decks. + self.exporter.did = None + self.exporter.cids = self.cids + else: + self.exporter.cids = None +### ending change + name = self.decks[self.frm.deck.currentIndex()] + self.exporter.did = self.col.decks.id(name) + if self.isVerbatim: + name = time.strftime("-%Y-%m-%d@%H-%M-%S", + time.localtime(time.time())) + deck_name = _("collection")+name + else: + # Get deck name and remove invalid filename characters + deck_name = self.decks[self.frm.deck.currentIndex()] + deck_name = re.sub('[\\\\/?<>:*|"^]', '_', deck_name) + + if not self.isVerbatim and self.isApkg and self.exporter.includeSched and self.col.schedVer() == 2: + showInfo("Please switch to the regular scheduler before exporting a single deck .apkg with scheduling.") + return + + filename = '{0}{1}'.format(deck_name, self.exporter.ext) + while 1: + file = getSaveFile(self, _("Export"), "export", + self.exporter.key, self.exporter.ext, + fname=filename) + if not file: + return + if checkInvalidFilename(os.path.basename(file), dirsep=False): + continue + break + self.hide() + if file: + self.mw.progress.start(immediate=True) + try: + f = open(file, "wb") + f.close() + except (OSError, IOError) as e: + showWarning(_("Couldn't save file: %s") % str(e)) + else: + os.unlink(file) + exportedMedia = lambda cnt: self.mw.progress.update( + label=ngettext("Exported %d media file", + "Exported %d media files", cnt) % cnt + ) + addHook("exportedMediaFiles", exportedMedia) + self.exporter.exportInto(file) + remHook("exportedMediaFiles", exportedMedia) + period = 3000 + if self.isVerbatim: + msg = _("Collection exported.") + else: + if self.isTextNote: + msg = ngettext("%d note exported.", "%d notes exported.", + self.exporter.count) % self.exporter.count + else: + msg = ngettext("%d card exported.", "%d cards exported.", + self.exporter.count) % self.exporter.count + tooltip(msg, period=period) + finally: + self.mw.progress.finish() + QDialog.accept(self) +ExportDialog.accept = accept diff --git a/addons/1983204951/browser.py b/addons/1983204951/browser.py new file mode 100644 index 00000000000..d2f20ed14a3 --- /dev/null +++ b/addons/1983204951/browser.py @@ -0,0 +1,12 @@ +from PyQt5.QtWidgets import QAction +from anki.hooks import addHook +from aqt.exporting import ExportDialog +from aqt.qt import QKeySequence + +def setupMenu(browser): + a = QAction("Export selection", browser) + a.setShortcut(QKeySequence("Ctrl+Shift+E")) + a.triggered.connect(lambda : ExportDialog(mw,cids=browser.selectedCards())) + browser.form.menuEdit.addAction(a) + +addHook("browser.setupMenus", setupMenu) diff --git a/addons/1983204951/config.json b/addons/1983204951/config.json new file mode 100644 index 00000000000..dc08d24b0c3 --- /dev/null +++ b/addons/1983204951/config.json @@ -0,0 +1,8 @@ +{ + "siblings":{ + "Cards in Plain Text": false, + "Notes in Plain Text":null, + "Anki Deck Package": null, + "default": true + } +} diff --git a/addons/1983204951/config.md b/addons/1983204951/config.md new file mode 100644 index 00000000000..946b27503b7 --- /dev/null +++ b/addons/1983204951/config.md @@ -0,0 +1,13 @@ +"siblings": A mapping stating, for each kind of export option, whether you want that cards are exported with their siblings (other cards from the same note). +* null means that you use the "default" setting. +* true means that, when you export a card, you export their siblings. +* false means that you can export cards without their siblings. + +The entry of the "siblings" mapping are keys of exporter, or the constant "default". To control exporter created by other add-on, check their code to find their "key" value, and add it to this mapping. The default values are: +* "default": the default value, when no other are used. +* "Notes in Plain Text": exporting notes as txt files. +* "Cards in Plain Text": exporting cards as txt files. +* "Anki Deck Package": exporting a .apkg file +If another exporter is added by an add-on, add its key to this mapping + +"Anki 2.0 Deck" is not listed. Indeed, when the entire collection is exported, this parameter becomes useless. \ No newline at end of file diff --git a/addons/1983204951/meta.json b/addons/1983204951/meta.json new file mode 100644 index 00000000000..3f9fde007fe --- /dev/null +++ b/addons/1983204951/meta.json @@ -0,0 +1 @@ +{"name": "Export cards selected in the Browser", "mod": 1552544145} \ No newline at end of file diff --git a/addons/1983204951/zipping b/addons/1983204951/zipping new file mode 100755 index 00000000000..7cff3f55765 --- /dev/null +++ b/addons/1983204951/zipping @@ -0,0 +1,2 @@ +rm exportFromBrowser.zip +zip exportFromBrowser.zip *py README.md LICENSE config* zipping --exclude \*~ \ No newline at end of file diff --git a/anki/cards.py b/anki/cards.py index 22fe41431f2..8cfc0b9c7ea 100644 --- a/anki/cards.py +++ b/anki/cards.py @@ -5,7 +5,7 @@ import time from anki.hooks import runHook -from anki.utils import intTime, timestampID, joinFields +from anki.utils import intTime, timestampID, joinFields, ids2str from anki.consts import * # Cards @@ -298,3 +298,8 @@ def userFlag(self): def setUserFlag(self, flag): assert 0 <= flag <= 7 self.flags = (self.flags & ~0b111) | flag + +def siblings(cids): + """Return the siblings of cards whose id is in cids""" + siblings = mw.col.db.list(f"select id from cards where nid in (select nid from cards where id in {ids2str(cids)})") + return siblings diff --git a/anki/exporting.py b/anki/exporting.py index 9c29dec7952..08595dd2d19 100644 --- a/anki/exporting.py +++ b/anki/exporting.py @@ -9,6 +9,7 @@ from anki.utils import ids2str, splitFields, namedtmp, stripHTML from anki.hooks import runHook from anki import Collection +from anki.cards import siblings class Exporter: """An abstract class. Inherited by class actually doing some kind of export. @@ -21,6 +22,7 @@ def __init__(self, col, did=None): #Currently, did is never set during initialisation. self.col = col self.did = did + self.cids = None def doExport(self, path): raise Exception("not implemented") @@ -69,13 +71,18 @@ def stripHTML(self, text): def cardIds(self): """card ids of cards in deck self.did if it is set, all ids otherwise.""" - if not self.did: + if self.cids is not None: + cids= self.cids + elif not self.did: cids = self.col.db.list("select id from cards") else: cids = self.col.decks.cids(self.did, children=True) self.count = len(cids) + if self.col.conf.get("exportSiblings", False): + cids = siblings(cids) return cids + # Cards as TSV ###################################################################### diff --git a/aqt/addons.py b/aqt/addons.py index 70275bee3db..6da11be4f51 100644 --- a/aqt/addons.py +++ b/aqt/addons.py @@ -901,6 +901,7 @@ def __hash__(self): """ Set of characteristic of Add-ons incorporated here""" incorporatedAddonsSet = { + Addon("Export cards selected in the Browser", 1983204951, 1560768960, "f8990da153af2745078e7b3c33854d01cb9fa304", "https://github.com/Arthur-Milchior/anki-export-from-browser"), } incorporatedAddonsDict = {**{addon.name: addon for addon in incorporatedAddonsSet if addon.name}, diff --git a/aqt/browser.py b/aqt/browser.py index f0e74fef0a0..4ed97ba711d 100644 --- a/aqt/browser.py +++ b/aqt/browser.py @@ -14,6 +14,7 @@ from aqt.qt import * import anki import aqt.forms +from aqt.exporting import ExportDialog from anki.utils import fmtTimeSpan, ids2str, htmlToTextLine, \ isWin, intTime, \ isMac, bodyClass @@ -458,6 +459,7 @@ def setupMenus(self): f.actionOrange_Flag.triggered.connect(lambda: self.onSetFlag(2)) f.actionGreen_Flag.triggered.connect(lambda: self.onSetFlag(3)) f.actionBlue_Flag.triggered.connect(lambda: self.onSetFlag(4)) + f.action_Export.triggered.connect(lambda: ExportDialog(self.mw, cids=self.selectedCards())) # jumps f.actionPreviousCard.triggered.connect(self.onPreviousCard) f.actionNextCard.triggered.connect(self.onNextCard) diff --git a/aqt/exporting.py b/aqt/exporting.py index 8199a294e1d..ecbd9dd3ba4 100644 --- a/aqt/exporting.py +++ b/aqt/exporting.py @@ -27,26 +27,32 @@ class ExportDialog(QDialog): - def __init__(self, mw, did=None): + def __init__(self, mw, did=None, cids=None): + """ + cids -- the cards selected, if it's opened from the browser + """ QDialog.__init__(self, mw, Qt.Window) self.mw = mw + self.cids = cids self.col = mw.col self.frm = aqt.forms.exporting.Ui_ExportDialog() self.frm.setupUi(self) self.exporter = None - self.setup(did) + self.setup(did, cids) self.exec_() - def setup(self, did): - """ + def setup(self, did = None, cids = None): + """Set the GUI such that, by default, it exports whole collection. Or deck did. Or cards cids. keyword arguments: - did -- if None, then export whole anki. If did, export this deck (at least as default). + did -- If did is not None, export this deck. + cids -- If cids is not None, export those cards. + """ self.exporters = exporters() # if a deck specified, start with .apkg type selected idx = 0 - if did: + if did or cids: for c, (k,e) in enumerate(self.exporters): if e.ext == ".apkg": idx = c @@ -56,7 +62,11 @@ def setup(self, did): self.frm.format.activated.connect(self.exporterChanged) self.exporterChanged(idx) # deck list - self.decks = [_("All Decks")] + sorted(self.col.decks.allNames()) + self.decks = [_("All Decks")] + if cids: + bs=_("Browser's selection") + self.decks.append(bs) + self.decks = self.decks + sorted(self.col.decks.allNames()) self.frm.deck.addItems(self.decks) # save button b = QPushButton(_("Export...")) @@ -96,9 +106,14 @@ def accept(self): self.frm.includeTags.isChecked()) self.exporter.includeHTML = ( self.frm.includeHTML.isChecked()) - if not self.frm.deck.currentIndex(): + if self.frm.deck.currentIndex() == 0: #position 0 means: all decks. + self.exporter.did = None + self.exporter.cids = None + elif self.frm.deck.currentIndex() == 1 and self.cids is not None:#position 1 means: selected decks. self.exporter.did = None + self.exporter.cids = self.cids else: + self.exporter.cids = None name = self.decks[self.frm.deck.currentIndex()] self.exporter.did = self.col.decks.id(name) if self.isVerbatim: diff --git a/aqt/preferences.py b/aqt/preferences.py index 32901cce08c..1b7f526e4ee 100644 --- a/aqt/preferences.py +++ b/aqt/preferences.py @@ -272,6 +272,7 @@ def updateOneOption(self, name, default=False, check=True, sync=True): storeValue[name] = value extraOptions = [ + ("exportSiblings",), ] def setupExtra(self): diff --git a/designer/browser.ui b/designer/browser.ui index 71ddbf188e3..09c68ece5f7 100644 --- a/designer/browser.ui +++ b/designer/browser.ui @@ -236,7 +236,7 @@ 0 0 750 - 22 + 20 @@ -295,6 +295,7 @@ + @@ -599,6 +600,14 @@ Ctrl+K + + + &Export Cards + + + Ctrl+Shift+E + + diff --git a/designer/preferences.ui b/designer/preferences.ui index 0009a7bf405..d6ea796c6ea 100644 --- a/designer/preferences.ui +++ b/designer/preferences.ui @@ -489,6 +489,13 @@ + + + + Export siblings of exported cards + + + diff --git a/difference.md b/difference.md index eeaf6f0f9f3..2d2739aae2c 100644 --- a/difference.md +++ b/difference.md @@ -2,3 +2,18 @@ This files list the difference between regular anki and this forked version. It also lists the different options in the Preferences's extra page. +## Export notes selected in the Browser (1983204951) +Selects some cards in the browser. Then `Cards>Export cards` allow you +to export the cards. This is similar to exporting a deck, except that +you have more fine control. You can essentially export anything that +you can query in the browser or that you can select. + +Note that, if you export a deck, or a selection of cards, and that you +export cards, you are also exporting the notes of those +cards. However, some cards may be missing. When importing those cards, +the missing cards will be generated, and will be totally new. You may +lose data this way. In the preference "Export siblings of exported +cards" allow you to avoid losing data, by exporting the siblings of +exported cards. The problem being, of course, that you may export +cards in decks you did not select. Thus, importing those cards may +potentially create more decks than expected. From a73a0baf550769c5b13c543eacebefe73b6425e8 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Sat, 6 Jul 2019 17:48:37 +0200 Subject: [PATCH 42/68] =?UTF-8?q?=C2=ABMap=20to=C2=BB=20is=20removed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- aqt/importing.py | 2 +- difference.md | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/aqt/importing.py b/aqt/importing.py index bfe2b014e12..64ba2f5ba7e 100644 --- a/aqt/importing.py +++ b/aqt/importing.py @@ -30,7 +30,7 @@ def __init__(self, mw, model, current): n = 0 setCurrent = False for field in self.model['flds']: - item = QListWidgetItem(_("Map to %s") % field['name']) + item = QListWidgetItem(field['name']) self.frm.fields.addItem(item) if current == field['name']: setCurrent = True diff --git a/difference.md b/difference.md index eeaf6f0f9f3..a372a107730 100644 --- a/difference.md +++ b/difference.md @@ -2,3 +2,6 @@ This files list the difference between regular anki and this forked version. It also lists the different options in the Preferences's extra page. +## Remove "Map to" in item import window for CSV (46741504) +Because of this text, the keyboard can't be used to search a field. I +thus remove it. From 01339f693e9d0fecaaf425d7a55744a9df936942 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Mon, 17 Jun 2019 16:18:21 +0200 Subject: [PATCH 43/68] Ensure that even if model change, add cards keep the same value --- addons/424778276/LICENSE | 674 ++++++++++++++++++++++++ addons/424778276/README.md | 47 ++ addons/424778276/__init__.py | 6 + addons/424778276/keepModelInAddCards.py | 124 +++++ addons/424778276/meta.json | 1 + aqt/addcards.py | 17 +- aqt/addons.py | 1 + aqt/modelchooser.py | 17 +- difference.md | 3 + 9 files changed, 880 insertions(+), 10 deletions(-) create mode 100755 addons/424778276/LICENSE create mode 100755 addons/424778276/README.md create mode 100755 addons/424778276/__init__.py create mode 100755 addons/424778276/keepModelInAddCards.py create mode 100755 addons/424778276/meta.json diff --git a/addons/424778276/LICENSE b/addons/424778276/LICENSE new file mode 100755 index 00000000000..94a9ed024d3 --- /dev/null +++ b/addons/424778276/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/addons/424778276/README.md b/addons/424778276/README.md new file mode 100755 index 00000000000..7ead7d62843 --- /dev/null +++ b/addons/424778276/README.md @@ -0,0 +1,47 @@ +# Keep model of add cards +## Rationale +For some reason, the note type in anki «add» window may suddenly +change. In the worst case, it may lead to losing data, if the new note +type has less fields than the old note type. It may also means you'll +submit the note in the wrong type if you didn't pay attention. + +More precisely, this may occur in the following cases: +* In the browser, if you change the note type of a note already + existing. +* If you use the importing window and change the note type. +* Using addon [Open multiple instances of the same + window](https://ankiweb.net/shared/info/354407385), when you change + the note type of a «add» window. +## Usage +This add-on ensure that the note type of a «add» window only change +when you choose a note type from the same window. + +## Internal +* In module `aqt.addcards`, `AddCards` is replaced by a + class. The new class inherits from the older one. It redefines + `__init__` and `_reject`, by calling the previous method, + and changing its hooks. It redefines + `setupChoosers`, `onReset`, `_addCards`, by copy-pasting + the previous code and doing some modification. Thus, it may be + incompatible with other add-ons also editing those notes. Finally it + creates a method `onResetSameModel`. +* Hooks currentModelChanged does not change anything in the "add" + window. "reset" still call the method "onReset", but a version which + does not change the model. +* The `ModelChooser` of the "add" window is a new class, inheriting + from `aqt.modelchooser.ModelChooser`. It redefines `__init__`, + calling the previous method and removing some hook. It redefine + `onModelChange` by copy-paste the old code, removing everything + affecting the environment, and affecting directly the current "add" + window. + +## Links, licence and credits + +Key |Value +-------------|------------------------------------------------------------------- +Copyright | Arthur Milchior +Based on | Anki code by Damien Elmes +License | GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +Source in | https://github.com/Arthur-Milchior/anki-keep-model-in-add-cards +Addon number | [424778276](https://ankiweb.net/shared/info/424778276) +Support me on| [![Ko-fi](https://ko-fi.com/img/Kofi_Logo_Blue.svg)](Ko-fi.com/arthurmilchior) or [![Patreon](http://www.milchior.fr/patreon.png)](https://www.patreon.com/bePatron?u=146206) diff --git a/addons/424778276/__init__.py b/addons/424778276/__init__.py new file mode 100755 index 00000000000..eb3adeda27e --- /dev/null +++ b/addons/424778276/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# Github: https://github.com/Arthur-Milchior/anki-keep-model-in-add-cards +# Original code from Anki, copyright Damien Elmes +# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +# Add-on number 424778276 https://ankiweb.net/shared/info/424778276 +from . import keepModelInAddCards diff --git a/addons/424778276/keepModelInAddCards.py b/addons/424778276/keepModelInAddCards.py new file mode 100755 index 00000000000..3fe998208b4 --- /dev/null +++ b/addons/424778276/keepModelInAddCards.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- +# Github: https://github.com/Arthur-Milchior/anki-keep-model-in-add-cards +# Original code from Anki, copyright Damien Elmes +# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +# Add-on number 424778276 https://ankiweb.net/shared/info/424778276 +from aqt.qt import QPushButton +from aqt.utils import tooltip +from anki.sound import clearAudioQueue +import aqt +from anki.lang import _ +import aqt.addcards +from anki.hooks import remHook, addHook +import aqt.modelchooser +from aqt.deckchooser import DeckChooser +from anki.notes import Note + +def debug(t): + #print(t) + pass + +class AddCards(aqt.addcards.AddCards): + def __init__(self,mw): + debug("Call newInit") + super().__init__(mw) + remHook("currentModelChanged",self.onModelChange) + remHook('reset', self.onReset) + addHook("reset",self.onResetSameModel) + + def setupChoosers(self): + debug("Call setupChoosers") + class ModelChooser(aqt.modelchooser.ModelChooser): + # def __init__(self, mw, widget, label=True): + # super().__init__( mw, widget, label=label) + # remHook('reset', self.onReset) + def updateModels(selfModel): + if hasattr(self,"editor"):#self's init has ended + modelName=self.editor.note._model["name"] + else:# initialisation of the window + modelName=selfModel.deck.models.current()['name'] + selfModel.models.setText(modelName) + + def onModelChange(selfModel): + """Open Choose Note Type window""" + #Method called when we want to change the current model + debug("Call newOnModelChange") + from aqt.studydeck import StudyDeck + current = selfModel.deck.models.current()['name'] + # edit button + edit = QPushButton(_("Manage"), clicked=selfModel.onEdit) + def nameFunc(): + return sorted(selfModel.deck.models.allNames()) + ret = StudyDeck( + selfModel.mw, names=nameFunc, + accept=_("Choose"), title=_("Choose Note Type"), + help="_notes", current=current, parent=selfModel.widget, + buttons=[edit], cancel=True, geomKey="selectModel") + if not ret.name: + return + m = selfModel.deck.models.byName(ret.name) + selfModel.deck.conf['curModel'] = m['id'] + cdeck = selfModel.deck.decks.current() + cdeck['mid'] = m['id'] + selfModel.deck.decks.save(cdeck) + #runHook("currentModelChanged") + #selfModel.mw.reset() + ### New line: + debug("Call AddCard.onModelChange") + self.onModelChange() #this is onModelChange from card, and note from ModelChange + selfModel.updateModels() + self.modelChooser = ModelChooser( + self.mw, self.form.modelArea) + self.deckChooser = DeckChooser( + self.mw, self.form.deckArea) + + + def onReset(self, model=None, keep=False): + """Create a new note and set it. + + keyword arguments + model -- A model object. Used for the new note. + keep -- whether to keep sticky values from old note + """ + flds = note.model()['flds'] + # copy fields from old note + if oldNote: + if not keep: + self.removeTempNote(oldNote) + for n in range(len(note.fields)): + try: + if not keep or flds[n]['sticky']: + note.fields[n] = oldNote.fields[n] + else: + note.fields[n] = "" + except IndexError: + break + self.setAndFocusNote(note) + + def onResetSameModel(self,keep=False):#this is a new method + debug("Call onResetSameModel") + return self.onReset(model=self.editor.note._model,keep=keep) + + def _addCards(self): + """Adding the content of the fields as a new note. + + Assume that the content of the GUI saved in the model.""" + debug("Call _addCards") + self.editor.saveAddModeVars() + note = self.editor.note + note = self.addNote(note) + if not note: + return + tooltip(_("Added"), period=500) + # stop anything playing + clearAudioQueue() + self.onResetSameModel(keep=True)#Only difference is calling onResetSameModel instead of onReset + self.mw.col.autosave() + + def _reject(self): + debug("Call _reject") + remHook('reset', self.onResetSameModel) + super()._reject() +aqt.addcards.AddCards= AddCards +#The window opener contains information about the class, and not its adress. Thus it must be updated. +aqt.dialogs._dialogs["AddCards"]=[AddCards,None] diff --git a/addons/424778276/meta.json b/addons/424778276/meta.json new file mode 100755 index 00000000000..282f6a726c3 --- /dev/null +++ b/addons/424778276/meta.json @@ -0,0 +1 @@ +{"name": "Keep model of add cards", "mod": 1553438887} \ No newline at end of file diff --git a/aqt/addcards.py b/aqt/addcards.py index 19c00d83fcb..2344851ae8e 100644 --- a/aqt/addcards.py +++ b/aqt/addcards.py @@ -30,8 +30,7 @@ def __init__(self, mw): self.onReset() self.history = [] restoreGeom(self, "add") - addHook('reset', self.onReset) - addHook('currentModelChanged', self.onModelChange) + addHook("reset",lambda: self.onResetSameModel) addCloseShortcut(self) self.show() @@ -41,7 +40,7 @@ def setupEditor(self): def setupChoosers(self): self.modelChooser = aqt.modelchooser.ModelChooser( - self.mw, self.form.modelArea) + self.mw, self.form.modelArea, addCardWindow = self) self.deckChooser = aqt.deckchooser.DeckChooser( self.mw, self.form.deckArea) @@ -106,6 +105,9 @@ def onModelChange(self): self.removeTempNote(oldNote) self.editor.setNote(note) + def onResetSameModel(self,keep=False): + return self.onReset(model=self.editor.note._model, keep=keep) + def onReset(self, model=None, keep=False): """Create a new note and set it with the current field values. @@ -119,7 +121,10 @@ def onReset(self, model=None, keep=False): #Called with default keep __init__, from hook "reset" #Meaning of the word keep guessed. Not clear. oldNote = self.editor.note - note = self.mw.col.newNote() + if model is None: + note = self.mw.col.newNote() + else:#Difference is here. If model given as argument, it is used + note = Note(self.mw.col, model=model) flds = note.model()['flds'] # copy fields from old note if oldNote: @@ -208,7 +213,7 @@ def _addCards(self): tooltip(_("Added"), period=500) # stop anything playing clearAudioQueue() - self.onReset(keep=True) + self.onResetSameModel(keep=True) self.mw.col.autosave() def keyPressEvent(self, evt): @@ -229,7 +234,7 @@ def _reject(self): """Close the window. Don't check whether data will be lost""" - remHook('reset', self.onReset) + remHook('reset', self.onResetSameModel) remHook('currentModelChanged', self.onModelChange) clearAudioQueue() self.removeTempNote(self.editor.note) diff --git a/aqt/addons.py b/aqt/addons.py index 70275bee3db..5a7ea0ab4a9 100644 --- a/aqt/addons.py +++ b/aqt/addons.py @@ -901,6 +901,7 @@ def __hash__(self): """ Set of characteristic of Add-ons incorporated here""" incorporatedAddonsSet = { + Addon("Keep model of add cards", 424778276, 1553438887, "64bdf3c7d8e252d6f69f0a423d2db3c23ce6bc04", "https://github.com/Arthur-Milchior/anki-keep-model-in-add-cards"), } incorporatedAddonsDict = {**{addon.name: addon for addon in incorporatedAddonsSet if addon.name}, diff --git a/aqt/modelchooser.py b/aqt/modelchooser.py index 5db821d8951..b88eba5ae01 100644 --- a/aqt/modelchooser.py +++ b/aqt/modelchooser.py @@ -19,10 +19,11 @@ class ModelChooser(QHBoxLayout): widget -- the button used to open this window. It contains the name of the current model. """ - def __init__(self, mw, widget, label=True): + def __init__(self, mw, widget, label=True, addCardWindow = None): QHBoxLayout.__init__(self) self.widget = widget self.mw = mw + self.addCardWindow = addCardWindow self.deck = mw.col self.label = label self.setContentsMargins(0,0,0,0) @@ -89,10 +90,18 @@ def nameFunc(): cdeck = self.deck.decks.current() cdeck['mid'] = m['id'] self.deck.decks.save(cdeck) - runHook("currentModelChanged") - self.mw.reset() + if self.addCardWindow: + runHook("currentModelChanged") + self.mw.reset() + else: + self.addCardWindow.onModelChange() #this is onModelChange from card, and note from ModelChange + self.updateModels() def updateModels(self): """Change the button's text so that it has the name of the current model.""" - self.models.setText(self.deck.models.current()['name']) + if hasattr(self,"editor") or (self.addCardWindow is not None):#self's init has ended + modelName=self.addCardWindow.editor.note._model["name"] + else:# initialisation of the window + modelName=self.deck.models.current()['name'] + self.models.setText(modelName) diff --git a/difference.md b/difference.md index eeaf6f0f9f3..d86fb0dfca9 100644 --- a/difference.md +++ b/difference.md @@ -2,3 +2,6 @@ This files list the difference between regular anki and this forked version. It also lists the different options in the Preferences's extra page. +## Add card: change note only if you ask for it (424778276) +Yeah, this description seems ridiculous. But actually, anki does not +respect this. From d388cd24e3fb6bb0b24175f9de3ced4161977c8f Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Fri, 12 Jul 2019 00:23:31 +0200 Subject: [PATCH 44/68] debugging --- aqt/addcards.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/aqt/addcards.py b/aqt/addcards.py index 2344851ae8e..13e03989330 100644 --- a/aqt/addcards.py +++ b/aqt/addcards.py @@ -3,6 +3,7 @@ # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html """The window obtained from main by pressing A, or clicking on "Add".""" from anki.lang import _ +from anki.notes import Note from aqt.qt import * import aqt.forms @@ -24,10 +25,10 @@ def __init__(self, mw): self.setWindowTitle(_("Add")) self.setMinimumHeight(300) self.setMinimumWidth(400) - self.setupChoosers() self.setupEditor() self.setupButtons() self.onReset() + self.setupChoosers() self.history = [] restoreGeom(self, "add") addHook("reset",lambda: self.onResetSameModel) From cd63a6c2c7a1e831fa5eb356f230b31e1a8f584a Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Mon, 17 Jun 2019 14:17:44 +0200 Subject: [PATCH 45/68] Keep the notes without cards --- addons/2018640062/LICENSE | 674 ++++++++++++++++++++++++++++++++++ addons/2018640062/README.md | 47 +++ addons/2018640062/__init__.py | 7 + addons/2018640062/init.py | 76 ++++ addons/2018640062/meta.json | 1 + aqt/addons.py | 1 + aqt/main.py | 61 ++- aqt/preferences.py | 1 + designer/preferences.ui | 11 +- difference.md | 8 + 10 files changed, 878 insertions(+), 9 deletions(-) create mode 100644 addons/2018640062/LICENSE create mode 100644 addons/2018640062/README.md create mode 100644 addons/2018640062/__init__.py create mode 100644 addons/2018640062/init.py create mode 100644 addons/2018640062/meta.json diff --git a/addons/2018640062/LICENSE b/addons/2018640062/LICENSE new file mode 100644 index 00000000000..f288702d2fa --- /dev/null +++ b/addons/2018640062/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/addons/2018640062/README.md b/addons/2018640062/README.md new file mode 100644 index 00000000000..c714bc3e345 --- /dev/null +++ b/addons/2018640062/README.md @@ -0,0 +1,47 @@ +# Tag notes with no card +## Rationale +Sometime, you press «Empty cards...», and anki also delete +note. Because when a note has no more card, anki deletes it. (see +[https://github.com/Arthur-Milchior/anki/blob/master/documentation/deletion.md] +for more information about deletion) + +However, maybe you didn't actually want to delete the whole note and +lose its entire content. May be you thought anki were going to delete +a few card which became empty, not realizing it will in fact delete +EVERY cards, and thus delete the note, and its fields, and its tag... + +With this add-on, when you press «Empty cards», cards are deleted as +usual, with one exception. If deleting the cards leave the note empty, +then the cards are not deleted. Instead, the tag NoteWithNoCard is +added to those notes, and the browser is opened to show those notes. + +You can still delete those notes if that's what you want, but you have +either to disable this add-on and restart anki, or to delete them +manually from the browser. + +## Warning +The tag NoteWithNoCard is automatically removed from notes which now +have a card, when you use «empty cards». This is probably what you +want, so that the tag states the truth. + +Note that a note with no cards can also occurs because of the +synchronization process. In which card, this add-on won't save the +note. Instead, you can save it using add-on +[1135180054](https://ankiweb.net/shared/info/1135180054). + +## Version 2.0 +Port by [lovac42](https://github.com/lovac42/anki-keep-empty-note) + +## Internal +This add-on modifies the method aqt.main.AnkiQt.onEmptyCards. + +## Links, licence and credits + +Key |Value +------------|------------------------------------------------------------------- +Copyright | Arthur Milchior +Based on | Anki code by Damien Elmes +License | GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +Source in | https://github.com/Arthur-Milchior/anki-keep-empty-note +Addon number| [2018640062](https://ankiweb.net/shared/info/2018640062) +Support me on| [![Ko-fi](https://ko-fi.com/img/Kofi_Logo_Blue.svg)](Ko-fi.com/arthurmilchior) or [![Patreon](http://www.milchior.fr/patreon.png)](https://www.patreon.com/bePatron?u=146206) diff --git a/addons/2018640062/__init__.py b/addons/2018640062/__init__.py new file mode 100644 index 00000000000..17bdacfeef5 --- /dev/null +++ b/addons/2018640062/__init__.py @@ -0,0 +1,7 @@ +from .init import onEmptyCards + +from aqt.main import AnkiQt +from aqt import mw +AnkiQt.onEmptyCards = onEmptyCards +mw.form.actionEmptyCards.triggered.disconnect() +mw.form.actionEmptyCards.triggered.connect(mw.onEmptyCards) diff --git a/addons/2018640062/init.py b/addons/2018640062/init.py new file mode 100644 index 00000000000..75a48e84ff7 --- /dev/null +++ b/addons/2018640062/init.py @@ -0,0 +1,76 @@ +from aqt.main import * +from anki.lang import _ +from aqt import mw, dialogs + +def onEmptyCards(self): + """Method called by Tools>Empty Cards...""" + self.progress.start(immediate=True) + print("Calling new onEmptyCards") + cids = set(self.col.emptyCids()) #change here to make a set + if not cids: + self.progress.finish() + tooltip(_("No empty cards.")) + return + print(f"Calling emptyCardReport from new onEmptyCards with cids {cids}") + report = self.col.emptyCardReport(cids) + self.progress.finish() + part1 = ngettext("%d card", "%d cards", len(cids)) % len(cids) + part1 = _("%s to delete:") % part1 + diag, box = showText(part1 + "\n\n" + report, run=False, + geomKey="emptyCards") + box.addButton(_("Delete Cards"), QDialogButtonBox.AcceptRole) + box.button(QDialogButtonBox.Close).setDefault(True) + def onDelete(): + nonlocal cids + print(f"Calling new onDelete with cids {cids}") + saveGeom(diag, "emptyCards") + QDialog.accept(diag) + self.checkpoint(_("Delete Empty")) + # Beginning of changes + nidToCidsToDelete = dict() + for cid in cids: + card = self.col.getCard(cid) + note = card.note() + nid = note.id + if nid not in nidToCidsToDelete: + print(f"note {nid} not yet in nidToCidsToDelete. Thus adding it") + nidToCidsToDelete[nid] = set() + else: + print(f"note {nid} already in nidToCidsToDelete.") + nidToCidsToDelete[nid].add(cid) + print(f"Adding card {cid} to note {nid}.") + emptyNids = set() + cardsOfEmptyNotes = set() + for nid, cidsToDeleteOfNote in nidToCidsToDelete.items(): + note = self.col.getNote(nid) + cidsOfNids = set([card.id for card in note.cards()]) + print(f"In note {nid}, the cards are {cidsOfNids}, and the cards to delete are {cidsToDeleteOfNote}") + if cidsOfNids == cidsToDeleteOfNote: + print(f"Both sets are equal") + emptyNids.add(note.id) + cids -= cidsOfNids + else: + print(f"Both sets are different") + self.col.remCards(cids, notes = False) + nidsWithTag = set(self.col.findNotes("tag:NoteWithNoCard")) + print (f"emptyNids is {emptyNids}, nidsWithTag is {nidsWithTag}") + for nid in emptyNids - nidsWithTag: + note = self.col.getNote(nid) + note.addTag("NoteWithNoCard") + print(f"Adding tag to note {note.id}") + note.flush() + for nid in nidsWithTag - emptyNids: + note = self.col.getNote(nid) + note.delTag("NoteWithNoCard") + print(f"Removing tag from note {note.id}") + note.flush() + if emptyNids: + showWarning(f"""{len(emptyNids)} note(s) should have been deleted because they had no more cards. They now have the tag "NoteWithNoCard". Please go check them. Then either edit them to save their content, or delete them from the browser.""") + browser = dialogs.open("Browser", mw) + browser.form.searchEdit.lineEdit().setText("tag:NoteWithNoCard") + browser.onSearchActivated() + # end of changes + tooltip(ngettext("%d card deleted.", "%d cards deleted.", len(cids)) % len(cids)) + self.reset() + box.accepted.connect(onDelete) + diag.show() diff --git a/addons/2018640062/meta.json b/addons/2018640062/meta.json new file mode 100644 index 00000000000..491e5d595ea --- /dev/null +++ b/addons/2018640062/meta.json @@ -0,0 +1 @@ +{"name": "If a note has no more card warns instead of deleting it", "mod": 1560126140} \ No newline at end of file diff --git a/aqt/addons.py b/aqt/addons.py index 70275bee3db..6d601d9d46a 100644 --- a/aqt/addons.py +++ b/aqt/addons.py @@ -901,6 +901,7 @@ def __hash__(self): """ Set of characteristic of Add-ons incorporated here""" incorporatedAddonsSet = { + Addon("If a note has no more card warns instead of deleting it", 2018640062, 1560126140, "4a854242d06a05b2ca801a0afc29760682004782", "https://github.com/Arthur-Milchior/anki-keep-empty-note"), } incorporatedAddonsDict = {**{addon.name: addon for addon in incorporatedAddonsSet if addon.name}, diff --git a/aqt/main.py b/aqt/main.py index 4525ba78078..20bb6a4a4be 100644 --- a/aqt/main.py +++ b/aqt/main.py @@ -1245,7 +1245,7 @@ def onEmptyCards(self): """Method called by Tools>Empty Cards...""" self.progress.start(immediate=True) - cids = self.col.emptyCids() + cids = set(self.col.emptyCids()) if not cids: self.progress.finish() tooltip(_("No empty cards.")) @@ -1258,15 +1258,62 @@ def onEmptyCards(self): geomKey="emptyCards") box.addButton(_("Delete Cards"), QDialogButtonBox.AcceptRole) box.button(QDialogButtonBox.Close).setDefault(True) - def onDelete(): - saveGeom(diag, "emptyCards") - QDialog.accept(diag) - self.checkpoint(_("Delete Empty")) + + box.accepted.connect(lambda: self.onDelete(cids, diag)) + diag.show() + + def onDelete(self, cids, diag): + """ + Delete cards with ids in cids. Close diag. + + If a note has no more card, either delete it or warn, depending on preferences. + """ + saveGeom(diag, "emptyCards") + QDialog.accept(diag) + self.checkpoint(_("Delete Empty")) + if not self.col.conf.get("keepEmptyNote", True): self.col.remCards(cids) tooltip(ngettext("%d card deleted.", "%d cards deleted.", len(cids)) % len(cids)) self.reset() - box.accepted.connect(onDelete) - diag.show() + + # Create a dic associating to each nid the cids to delete. + nidToCidsToDelete = dict() + for cid in cids: + card = self.col.getCard(cid) + note = card.note() + nid = note.id + if nid not in nidToCidsToDelete: + nidToCidsToDelete[nid] = set() + nidToCidsToDelete[nid].add(cid) + + # Compute the set of empty notes. Keep their cards + emptyNids = set() + for nid, cidsToDeleteOfNote in nidToCidsToDelete.items(): + note = self.col.getNote(nid) + cidsOfNids = set([card.id for card in note.cards()]) + if cidsOfNids == cidsToDeleteOfNote: + emptyNids.add(note.id) + cids -= cidsOfNids + + self.col.remCards(cids, notes = False) + + # Deal with tags + nidsWithTag = set(self.col.findNotes("tag:NoteWithNoCard")) + for nid in emptyNids - nidsWithTag: + note = self.col.getNote(nid) + note.addTag("NoteWithNoCard") + note.flush() + for nid in nidsWithTag - emptyNids: + note = self.col.getNote(nid) + note.delTag("NoteWithNoCard") + note.flush() + + # Warn about notes without cards. + if emptyNids: + showWarning(f"""{len(emptyNids)} note(s) should have been deleted because they had no more cards. They now have the tag "NoteWithNoCard". Please go check them. Then either edit them to save their content, or delete them from the browser.""") + browser = aqt.dialogs.open("Browser", self) + browser.form.searchEdit.lineEdit().setText("tag:NoteWithNoCard") + browser.onSearchActivated() # Debugging ###################################################################### diff --git a/aqt/preferences.py b/aqt/preferences.py index 32901cce08c..f2438e82758 100644 --- a/aqt/preferences.py +++ b/aqt/preferences.py @@ -272,6 +272,7 @@ def updateOneOption(self, name, default=False, check=True, sync=True): storeValue[name] = value extraOptions = [ + ("keepEmptyNote", True), ] def setupExtra(self): diff --git a/designer/preferences.ui b/designer/preferences.ui index 0009a7bf405..dc67de5e022 100644 --- a/designer/preferences.ui +++ b/designer/preferences.ui @@ -7,7 +7,7 @@ 0 0 423 - 508 + 552 @@ -20,7 +20,7 @@ Qt::StrongFocus - 0 + 3 @@ -489,6 +489,13 @@ + + + + When a note has no more card, warns instead of deleting it. + + + diff --git a/difference.md b/difference.md index eeaf6f0f9f3..0f3eb5e0b1a 100644 --- a/difference.md +++ b/difference.md @@ -2,3 +2,11 @@ This files list the difference between regular anki and this forked version. It also lists the different options in the Preferences's extra page. +## Keeping note which have no cards (2018640062) +If you do «Empty cards», and a note has no more card, then you see a +warning, and the browser open to show you what notes have this +problem. You can thus correct them and avoid loosing the content of +their fields. + +If you want to remove this feature, and have anki's default, uncheck +«Keep note without any card and warn» in the preferences. From a0f6f59e48c0cf925d9b224e17ca56deb38a72a2 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Mon, 17 Jun 2019 16:40:12 +0200 Subject: [PATCH 46/68] Merge new line in json --- addons/112201952/README.md | 49 ++++++++++ addons/112201952/__init__.py | 8 ++ addons/112201952/debug.py | 168 +++++++++++++++++++++++++++++++++++ addons/112201952/meta.json | 1 + addons/112201952/newline.py | 75 ++++++++++++++++ anki/utils.py | 22 +++++ aqt/addons.py | 33 ++++++- difference.md | 4 + 8 files changed, 358 insertions(+), 2 deletions(-) create mode 100644 addons/112201952/README.md create mode 100644 addons/112201952/__init__.py create mode 100644 addons/112201952/debug.py create mode 100644 addons/112201952/meta.json create mode 100644 addons/112201952/newline.py diff --git a/addons/112201952/README.md b/addons/112201952/README.md new file mode 100644 index 00000000000..72313101105 --- /dev/null +++ b/addons/112201952/README.md @@ -0,0 +1,49 @@ +# New line in strings in configurations/json +## Rationale +In standard json, string should not contain newlines. They should +contain "\n" instead, the standard symbols to represents a +newline. Which clearly create a readability problem. In particular +when an add-on configuration contains code (python, latex, whatever). + +Thus, I wanted to have the right to have newlines in strings in +json. And when I use the configuration editor, I want to see new +lines insteads of \n. +## Usage + +This add-on allow you to write new line in your string. The add-on +will replace them by "\n" before json load the configuration. This +works both for configuration in the file meta.json, and for the the +add-on manager's configuration editor. + +## Warning +Note that it will not put newlines in the configuration written +in meta.conf. Indeed, this would create at least two problems: +* We can't control the order in which add-ons are loaded. Thus all +add-ons loaded before this current add-on would fail when trying to +read their configuration. +* If you decide to uninstall this add-on, no configuration with new +lines could be read anymore. (In particular, this would happen if you +synchronize your configuration using another add-on). +## Internal +This add-on has an effect on ALL json strings read by the +program. New lines in strings will always be removed and replaced by +\n. Thus this add-on may affect more than configurations. However, +this add-on call the standard json.loads() function, thus even another +add-on affecting json.loads() should still be compatible with the +current addon. Futhermore, this add-on has no effect on valid +json. Thus the potential change would only be that some invalid json +would be accepted, it seems harmless. + +Furthermore, this add-on change ConfigEditor.updateText in module +aqt.addons. It does not call the previous function, thus the current +add-on will be incompatible with another add-on changing this method. + +## Links, licence and credits + +Key |Value +------------|------------------------------------------------------------------- +Copyright | Arthur Milchior +Based on | Anki code by Damien Elmes +License | GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +Source in | https://github.com/Arthur-Milchior/anki-json-new-line +Addon number| [112201952](https://ankiweb.net/shared/info/112201952) diff --git a/addons/112201952/__init__.py b/addons/112201952/__init__.py new file mode 100644 index 00000000000..a8f58190097 --- /dev/null +++ b/addons/112201952/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +# Copyright: Arthur Milchior +# Based on anki code by Damien Elmes +# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +# Source in https://github.com/Arthur-Milchior/anki-json-new-line +# Addon 112201952 + +from . import newline diff --git a/addons/112201952/debug.py b/addons/112201952/debug.py new file mode 100644 index 00000000000..00ece0aec0c --- /dev/null +++ b/addons/112201952/debug.py @@ -0,0 +1,168 @@ +import re +from inspect import stack +doMemoize = False + +#whether debug may be turned on eventually. Less efficient +mayDebug = False + +#Whether right debuging is on +shouldDebug = False +def startDebug(): + global shouldDebug + shouldDebug = True + print("Debug started") +def endDebug(): + global shouldDebug + shouldDebug = False + print("Debug ended") + +indentation = 0 +def debug(text, indentToAdd=0, force = False, level =1): + if not shouldDebug and not force: + return + global indentation + glob = stack()[level].frame.f_globals + loc = stack()[level].frame.f_locals + text = eval(f"""f"{text}" """,glob,loc) + indentToPrint = indentation + t = " "*indentToPrint + if indentToAdd>0: + t+= "{<" + space = " " + newline = "\n" + t+= re.sub(newline,newline+space,text) + print (t) + indentation +=indentToAdd + if indentToAdd<0: + indentToPrint +=indentToAdd + print((" "*indentToPrint)+">}") + +nbInsideThis = 0 +def debugInsideThisMethod(fun): + if not mayDebug: + return fun + def aux_debugInsideThisMethod(*args, **kwargs): + global nbInsideThis + startDebug() + nbInsideThis+=1 + ret = fun(*args, **kwargs) + nbInsideThis -=1 + if nbInsideThis ==0: + endDebug() + return ret + return aux_debugInsideThisMethod + +def debugOnlyThisMethod(fun): + return debugFun(fun, (lambda text,indentToAdd = 0: debug(text, indentToAdd, force = True, level=2))) + +def assertEqual(left, right): + if left == right: + return True + print(f"""\n\nReceived\n\"\"\"{left}\"\"\"\nwhich is distinct from expected\n\"\"\"{right}\"\"\"\n""") + if hasattr(left,"firstDifference"): + if hasattr(right,"firstDifference"): + pair = left.firstDifference(right) + if isinstance(pair,tuple): + left_dif,right_dif=pair + print(f"""\n\nThe first difference is\n\"\"\"{left_dif}\"\"\"\nand\n\"\"\"{right_dif}\"\"\"\n""") + elif isinstance(pair,None): + print("Strangely, firstDifference find no difference") + else: + assert False + else: + print("Only the first is a Gen") + elif hasattr(right,"firstDifference"): + print("Only the second is a Gen") + return False + +# def assertEqualString(left, right): +# glob = stack()[1].frame.f_globals +# loc = stack()[1].frame.f_locals +# # try: +# leftEval = eval(left, glob, loc) +# # except NameError as n: +# # print(f"""glob is {glob}""") +# # raise +# rightEval = eval(right,glob,loc) +# if leftEval == rightEval: +# return True +# print(f"""\n\n{left} evaluates as \n"{leftEval}".\n"{rightEval}"\n is the value of {right}, they are distinct.""") +# return False + +def assertType(element,types): + if not isinstance(types,list): + types = [types] + for typ in types: + if isinstance(element,typ): + return True + print(f""" "{element}"'s type is {type(element)}, which is not a subtype of {types}""") + return False + + +def debugFun(fun, debug=debug): + if not mayDebug: + return fun + def aux_debugFun(*args, **kwargs): + nonlocal debug + t = f"{fun.__qualname__}(" + first = False + def comma(text): + nonlocal first, t + if not first: + first = True + else: + t+=", " + t+=text + for arg in args: + comma(f"{arg}") + for kw in kwargs: + comma(f"{kw}={kwargs[kw]}") + t+=")" + debug("{t}",1) + ret = fun(*args, **kwargs) + debug("returns {ret}",-1) + return ret + aux_debugFun.__name__ = f"debug_{fun.__name__}" + aux_debugFun.__qualname__ = f"debug_{fun.__qualname__}" + return aux_debugFun + + +def debugInit(fun, debug=debug): + if not mayDebug: + return fun + def aux_debugInit(self, *args, **kwargs): + t = f"{fun.__name__}(" + needSeparator = False + def comma(text): + nonlocal needSeparator, t + if not needSeparator: + needSeparator = True + else: + t+=", " + t+=text + isSelf = True + for arg in args: + if isSelf: + isSelf = False + continue + comma(f"{arg}") + for kw in kwargs: + comma(f"{kw}={kwargs[kw]}") + t+=")" + debug("{t}",1) + fun(self, *args, **kwargs) + debug("returns {self}",-1) + aux_debugInit.__name__ = f"debug_{fun.__name__}" + aux_debugInit.__qualname__ = f"debug_{fun.__qualname__}" + return aux_debugInit + +def debugOnlyThisInit(fun): + return debugInit(fun, (lambda text,indentToAdd = 0: debug(text, indentToAdd, force = True, level=2))) + + +class ExceptionInverse(Exception): + def __init__(self,text): + self.text = "\n".join(reversed((str(text)+"\n").split("\n"))) + + def __str__(self): + return f"Exception: {self.text}" diff --git a/addons/112201952/meta.json b/addons/112201952/meta.json new file mode 100644 index 00000000000..385bf798a08 --- /dev/null +++ b/addons/112201952/meta.json @@ -0,0 +1 @@ +{"name": "Newline in strings in add-ons configurations", "mod": 1560116341} \ No newline at end of file diff --git a/addons/112201952/newline.py b/addons/112201952/newline.py new file mode 100644 index 00000000000..79b624c6715 --- /dev/null +++ b/addons/112201952/newline.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# Copyright: Arthur Milchior +# Based on anki code by Damien Elmes +# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +# Source in https://github.com/Arthur-Milchior/anki-json-new-line +# Addon number 11201952 +from aqt.addons import AddonManager, ConfigEditor +import sys +import json +import re +from .debug import debug, debugFun + +debug("json") +oldLoads=json.loads +def newLoads(t,*args,**kwargs): + debug(f"newLoads «{t}»") + t_=correctJson(t) + debug(f"t_ is {t_}") + res = oldLoads(t_,*args,**kwargs) + debug("res is {res}") + # if res is None: + # print(f"«{t}» led to None") + #print(f"From «{t}» to «{res}»") + return res + +json.loads=newLoads + + +@debugFun +def correctJson(text): + """Text, with new lines replaced by \n when inside quotes""" + if not isinstance(text,str): + return text + def correctQuotedString(match): + string = match[0] + debug("Found string «{string}»") + return string.replace("\n","\\n") + res= re.sub(r'"(?:(?<=[^\\])(?:\\\\)*\\"|[^"])*"',correctQuotedString,text,re.M) + if res is None: + debug("«{text}» was sent to None") + return res + +def readableJson(text): + """Text, where \n are replaced with new line. Unless it's preceded by a odd number of \.""" + l=[] + numberOfSlashOdd=False + numberOfQuoteOdd=False + for char in text: + if char == "n" and numberOfQuoteOdd and numberOfSlashOdd: + l[-1]="\n" + debug("replacing last slash by newline") + else: + l.append(char) + if char=="\n": + char="newline" + debug(f"adding {char}") + + if char == "\"": + if not numberOfSlashOdd: + numberOfQuoteOdd = not numberOfQuoteOdd + debug(f"numberOfQuoteOdd is now {numberOfQuoteOdd}") + + if char == "\\": + numberOfSlashOdd = not numberOfSlashOdd + else: + numberOfSlashOdd = False + debug(f"numberOfSlashOdd is now {numberOfSlashOdd}") + return "".join(l) + + + +def updateText(self, conf): + self.form.editor.setPlainText( + readableJson(json.dumps(conf,sort_keys=True,indent=4, separators=(',', ': ')))) +ConfigEditor.updateText=updateText diff --git a/anki/utils.py b/anki/utils.py index bc52d186c91..25f4e5f9410 100644 --- a/anki/utils.py +++ b/anki/utils.py @@ -444,3 +444,25 @@ def versionWithBuild(): except: build = "dev" return "%s (%s)" % (version, build) + +# JSon +############################################################################## +# Allow to have newline in strings in JSON + +def correctJson(text): + """Text, with new lines replaced by \n when inside quotes""" + if not isinstance(text,str): + return text + def correctQuotedString(match): + string = match[0] + return string.replace("\n","\\n") + res= re.sub(r'"(?:(?<=[^\\])(?:\\\\)*\\"|[^"])*"',correctQuotedString,text,re.M) + return res + +oldLoads=json.loads +def newLoads(t,*args,**kwargs): + t_=correctJson(t) + res = oldLoads(t_,*args,**kwargs) + return res + +json.loads=newLoads diff --git a/aqt/addons.py b/aqt/addons.py index 70275bee3db..dae5d86a951 100644 --- a/aqt/addons.py +++ b/aqt/addons.py @@ -806,6 +806,35 @@ def accept(self): # Editing config ###################################################################### +def readableJson(text): + """Text, where \n are replaced with new line. Unless it's preceded by a odd number of \.""" + l=[] + numberOfSlashOdd=False + numberOfQuoteOdd=False + for char in text: + if char == "n" and numberOfQuoteOdd and numberOfSlashOdd: + l[-1]="\n" + debug("replacing last slash by newline") + else: + l.append(char) + if char=="\n": + char="newline" + debug(f"adding {char}") + + if char == "\"": + if not numberOfSlashOdd: + numberOfQuoteOdd = not numberOfQuoteOdd + debug(f"numberOfQuoteOdd is now {numberOfQuoteOdd}") + + if char == "\\": + numberOfSlashOdd = not numberOfSlashOdd + else: + numberOfSlashOdd = False + debug(f"numberOfSlashOdd is now {numberOfSlashOdd}") + return "".join(l) + + + class ConfigEditor(QDialog): def __init__(self, dlg, addon, conf): @@ -843,8 +872,7 @@ def updateHelp(self): def updateText(self, conf): self.form.editor.setPlainText( - json.dumps(conf, ensure_ascii=False, sort_keys=True, - indent=4, separators=(',', ': '))) + readableJson(json.dumps(conf,sort_keys=True,indent=4, separators=(',', ': ')))) def onClose(self): saveGeom(self, "addonconf") @@ -901,6 +929,7 @@ def __hash__(self): """ Set of characteristic of Add-ons incorporated here""" incorporatedAddonsSet = { + Addon("Newline in strings in add-ons configurations", 112201952, 1560116341, "c02ac9bbbc68212da3d2bccb65ad5599f9f5af97", "https://github.com/Arthur-Milchior/anki-json-new-line"), } incorporatedAddonsDict = {**{addon.name: addon for addon in incorporatedAddonsSet if addon.name}, diff --git a/difference.md b/difference.md index eeaf6f0f9f3..e642f476639 100644 --- a/difference.md +++ b/difference.md @@ -2,3 +2,7 @@ This files list the difference between regular anki and this forked version. It also lists the different options in the Preferences's extra page. +## New line in Json (112201952) +In order to lead configurations be easier to edit, this add-on allow +newline in json strings. It allow add newlines in the add-on +configuration editor. From 18ebf45f29ef628c6a8b9b065e1ff03470af29d8 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Mon, 17 Jun 2019 17:45:14 +0200 Subject: [PATCH 47/68] Allow to open the same window multiple time --- addons/354407385/README.md | 84 ++++++++++++++++++ addons/354407385/__init__.py | 7 ++ addons/354407385/config.json | 1 + addons/354407385/config.md | 10 +++ addons/354407385/meta.json | 1 + addons/354407385/multiple.py | 165 +++++++++++++++++++++++++++++++++++ aqt/__init__.py | 76 +++++++++++++--- aqt/addons.py | 1 + aqt/preferences.py | 4 +- designer/preferences.ui | 48 +++++++++- difference.md | 6 ++ 11 files changed, 391 insertions(+), 12 deletions(-) create mode 100755 addons/354407385/README.md create mode 100755 addons/354407385/__init__.py create mode 100755 addons/354407385/config.json create mode 100755 addons/354407385/config.md create mode 100755 addons/354407385/meta.json create mode 100755 addons/354407385/multiple.py diff --git a/addons/354407385/README.md b/addons/354407385/README.md new file mode 100755 index 00000000000..145af6a3407 --- /dev/null +++ b/addons/354407385/README.md @@ -0,0 +1,84 @@ +# Open multiple instances of the same window +## Rationale +May be you want to have a "add" window for each type of Note, so you +won't have to regularly change note type. + +Maybe you want to see simultaneusly the result of multiple searchs in +the browser. + +May be you started a note. Then you have an idea for another note, but +don't want to submit the first note already. Thus you need to note +adders. + +May be you want to see multiple time the about window (even if I must +confess I don't understand why you would want that). Anyway, this +add-on allow you to open most windows multiple time. + +## Usage +To open a new window of a kind, just do what you need to open the +window. + +If you want that some kind of window is opened only once, read section +"configuration" + +## Advice +Currently, when you change the note type of an «add cards» windows, +all types are changed. In order to fight this undesired behavior, you +should also install Add-On number [424778276](https://ankiweb.net/shared/info/424778276) + +## Warnings +It may be the case that when you change the configuration about a +window which is already opened, you'll see a message error. It should +not create real trouble. Please report otherwise. + +All opened «AddCards» must have the same note type. This is a big +restriction, which I hope to be able to solve. + +## Internal +This may only works with windows which uses aqt's dialog manager. In +general, they are big windows, whose have no direct effect on the +window calling it. I.e. it won't work with a prompt asking you to +confirm/cancel something, or to say «ok». It should work with the +browser. + +This add-on redefine: +* `aqt.__init__`'s class `DialogManager`. More precisely, the + new class inherits from the last one. When a window may be opened a + single time, the former method is called. +* `aqt.editcurrent`'s method `EditCurrent.onReset` is + redefined. Thus this add-on may be incompatible with other add-on + changing this. + + + +## Configuration +In the map "multiple", add «, "windowName" : false», where windowName +is the name of the window you want to see open only once. + +The most standard windows name are: +* AddCards +* Browser +* EditCurrent +* DeckStats +* About +* Preferences + +By default, you can't open the two last ones more than once. Because +this would make no sens. You can change the configuration if for some +reason you want to do it. + +## Version 2.0 +Please use add-on [Multiple 'Add' and 'Browser' Windows, with addon +numbers](https://ankiweb.net/shared/info/969743069) instead. +## Links, licence and credits + +Key |Value +------------|------------------------------------------------------------------- +Copyright | Arthur Milchior +Based on | Anki code by Damien Elmes +Requested by| ijgnord on [reddit](https://www.reddit.com/r/Anki/comments/9z4fuv/do_you_want_miss_some_addons_you_loved_in_anki_20/ea6f2lw/) +Original idea by | Webventure, addon number [969743069](https://ankiweb.net/shared/info/969743069) +License | GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +Source in | https://github.com/Arthur-Milchior/anki-Multiple-Windows +Addon number| [354407385](https://ankiweb.net/shared/info/354407385) +Support me on| [![Ko-fi](https://ko-fi.com/img/Kofi_Logo_Blue.svg)](Ko-fi.com/arthurmilchior) or [![Patreon](http://www.milchior.fr/patreon.png)](https://www.patreon.com/bePatron?u=146206) diff --git a/addons/354407385/__init__.py b/addons/354407385/__init__.py new file mode 100755 index 00000000000..a6589fc232d --- /dev/null +++ b/addons/354407385/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +# Copyright: Arthur Milchior arthur@milchior.fr +# encoding: utf8 +# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html +# Feel free to contribute to this code on https://github.com/Arthur-Milchior/anki-Multiple-Windows +# Add-on number 354407385 https://ankiweb.net/shared/info/354407385 +from . import multiple diff --git a/addons/354407385/config.json b/addons/354407385/config.json new file mode 100755 index 00000000000..38609bc7817 --- /dev/null +++ b/addons/354407385/config.json @@ -0,0 +1 @@ +{"multiple":{"default":true, "About":false, "Preferences":false }} diff --git a/addons/354407385/config.md b/addons/354407385/config.md new file mode 100755 index 00000000000..6a977be78be --- /dev/null +++ b/addons/354407385/config.md @@ -0,0 +1,10 @@ +In dictionnary "multiple": +* WindowName is associated to the fact that multiple windows of this kind may be opened. +* "default" is the default value when the name of another window is not present. +The most standard windows name are: +* AddCards +* Browser +* EditCurrent +* DeckStats +* About +* Preferences diff --git a/addons/354407385/meta.json b/addons/354407385/meta.json new file mode 100755 index 00000000000..a359b6c6bb7 --- /dev/null +++ b/addons/354407385/meta.json @@ -0,0 +1 @@ +{"name": "Opening the same window multiple time", "mod": 1545364194, "config": {"multiple": {"About": false, "EditCurrent": true, "Preferences": false, "default": true}}} \ No newline at end of file diff --git a/addons/354407385/multiple.py b/addons/354407385/multiple.py new file mode 100755 index 00000000000..3ba43e94115 --- /dev/null +++ b/addons/354407385/multiple.py @@ -0,0 +1,165 @@ +# -*- coding: utf-8 -*- +# Copyright: Arthur Milchior arthur@milchior.fr +# encoding: utf8 +# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html +# Feel free to contribute to this code on https://github.com/Arthur-Milchior/anki-Multiple-Windows +# Add-on number 354407385 https://ankiweb.net/shared/info/354407385 +import aqt +from aqt import mw, DialogManager +import sip +from inspect import stack + +from aqt.editcurrent import EditCurrent +from anki.hooks import remHook + +def debug(t): + #print(t) + pass + +def shouldBeMultiple(name): + """Whether window name may have multiple copy. + + Ensure that ["multiple"] exsits in the configuration file. The default value being True. + """ + debug(f"Calling shouldBeMultiple({name})") + userOption = mw.addonManager.getConfig(__name__) + if "multiple" not in userOption: + userOption["multiple"]={"default": True} + debug(f"""Adding "multiple" to userOption""") + mw.addonManager.writeConfig(__name__,userOption) + multipleOption = userOption["multiple"] + debug(f"""multipleOption is {multipleOption}""") + if name in multipleOption: + debug(f"""{name} in multipleOption, its value is {multipleOption[name]}""") + return multipleOption[name] + elif "default" in multipleOption: + debug(f"""Not {name} but "default" in multipleOption, its value is {multipleOption["default"]}""") + return multipleOption["default"] + else: + debug(f"""Not {name} nor "default" in multipleOption. Returning True""") + return True + + +class DialogManagerMultiple(DialogManager): + """Associating to a window name a pair (as a list...) + + The element associated to WindowName Is composed of: + First element is the class to use to create the window WindowName. + Second element is always None + """ + # We inherits from aqt.DialogManager. Thus, if something is added to + # its _dialogs, we have access to it. + + # Every method are redefined, they use the parent's method when it makes sens. + + def __init__(self,oldDialog=None): + if oldDialog is not None: + DialogManagerMultiple._dialogs= oldDialog._dialogs + super().__init__() + + _openDialogs = list() + def open(self,name,*args): + """Open a new window, with name and args. + + Or reopen the window name, if it should be single in the + config, and is already opened. + """ + debug(f"Calling open({name},*args)") + function = self.openMany if shouldBeMultiple(name) else super().open + return function(name,*args) + + + def openMany(self, name, *args): + """Open a new window whose kind is name. + + keyword arguments: + args -- values passed to the opener. + name -- the name of the window to open + """ + debug(f"Calling openMany({name},{args})") + (creator, _) = self._dialogs[name] + instance = creator(*args) + self._openDialogs.append(instance) + return instance + + def markClosedMultiple(self): + debug(f"markClosedMultiple({self})") + caller = stack()[2].frame.f_locals['self'] + if caller in self._openDialogs: + debug(f"caller found") + self._openDialogs.remove(caller) + else: + debug(f"caller not found") + + def markClosed(self, name): + """Remove the window of windowName from the set of windows. """ + # If it is a window of kind single, then call super + # Otherwise, use inspect to figure out which is the caller + debug(f"Calling markClosed({name})") + if shouldBeMultiple(name): + self.markClosedMultiple() + else: + super().markClosed(name) + + def allClosed(self): + """ + Whether all windows (except the main window) are marked as + closed. + """ + debug(f"Calling allClosed()") + return self._openDialogs==[] and super().allClosed() + + + def closeAll(self, onsuccess): + """Close all windows (except the main one). Call onsuccess when it's done. + + Return True if some window needed closing. + None otherwise + + Keyword arguments: + onsuccess -- the function to call when the last window is closed. + """ + debug(f"Calling closeAll({onsuccess})") + def callback(): + """Call onsuccess if all window (except main) are closed.""" + print(f"Calling callback") + if self.allClosed(): + onsuccess() + else: + # still waiting for others to close + pass + if self.allClosed(): + onsuccess() + return + + for instance in self._openDialogs: + if not sip.isdeleted(instance):#It should be useless. I prefer to keep it to avoid erros + if getattr(instance, "silentlyClose", False): + instance.close() + callback() + else: + instance.closeWithCallback(callback) + + return super().closeAll(onsuccess) + + +aqt.DialogManager = DialogManagerMultiple +aqt.dialogs= DialogManagerMultiple(oldDialog=aqt.dialogs) +debug("Changing dialog manager") + +def onReset(self): + # lazy approach for now: throw away edits + try: + n = self.editor.note + n.load() + except: + # card's been deleted + remHook("reset", self.onReset) + self.editor.setNote(None) + self.mw.reset() + aqt.dialogs.markClosed("EditCurrent") + self.close() + return + self.editor.setNote(n) + +EditCurrent.onReset=onReset diff --git a/aqt/__init__.py b/aqt/__init__.py index a9796c26f70..726d42895ff 100644 --- a/aqt/__init__.py +++ b/aqt/__init__.py @@ -10,6 +10,7 @@ import builtins import locale import gettext +from inspect import stack from aqt.qt import * import anki.lang @@ -86,8 +87,37 @@ class DialogManager: "Preferences": [preferences.Preferences, None], } + """List of opened window. In order to close them all""" + _openDialogs = list() + + def isMultiple(self, name): + if "name" not in {"AddCards", "Browser", "EditCurrent"}: + name = "OtherWindows" + # no need to import mw, we are already in the aqt module + return mw.col.conf.get(f"{name}MultipleTime", True) def open(self, name, *args): + """Open a new window, with name and args. + + Or reopen the window name, if it should be single in the + config, and is already opened. + """ + function = self.openMany if self.isMultiple(name) else self.openSingle + return function(name,*args) + + def openMany(self, name, *args): + """Open a new window whose kind is name. + + keyword arguments: + args -- values passed to the opener. + name -- the name of the window to open + """ + (creator, _) = self._dialogs[name] + instance = creator(*args) + self._openDialogs.append(instance) + return instance + + def openSingle(self, name, *args): """Open a window of kind name. Open (and show) the one already opened, if it @@ -112,6 +142,24 @@ def open(self, name, *args): return instance def markClosed(self, name): + """Remove the window of windowName from the set of windows. """ + # If it is a window of kind single, then call super + # Otherwise, use inspect to figure out which is the caller + if self.isMultiple(name): + self.markClosedMultiple() + else: + self.markClosedSingle(name) + + def markClosedMultiple(self): + caller = stack()[2].frame.f_locals['self'] + if caller in self._openDialogs: + #caller found + self._openDialogs.remove(caller) + else: + pass + #caller not found + + def markClosedSingle(self, name): """Window name is now considered as closed. It removes the element from _dialogs.""" self._dialogs[name] = [self._dialogs[name][0], None] @@ -120,7 +168,7 @@ def allClosed(self): Whether all windows (except the main window) are marked as closed. """ - return not any(x[1] for x in self._dialogs.values()) + return self._openDialogs==[] and (not any(x[1] for x in self._dialogs.values())) def closeAll(self, onsuccess): """Close all windows (except the main one). Call onsuccess when it's done. @@ -132,24 +180,32 @@ def closeAll(self, onsuccess): onsuccess -- the function to call when the last window is closed. """ + def callback(): + """Call onsuccess if all window (except main) are closed.""" + if self.allClosed(): + onsuccess() + else: + # still waiting for others to close + pass # can we close immediately? if self.allClosed(): onsuccess() return # ask all windows to close and await a reply + ## Windows opened multiple time + for instance in self._openDialogs: + if not sip.isdeleted(instance):#It should be useless. I prefer to keep it to avoid erros + if getattr(instance, "silentlyClose", False): + instance.close() + callback() + else: + instance.closeWithCallback(callback) + + ## Windows opened a single time for (name, (creator, instance)) in self._dialogs.items(): if not instance: continue - - def callback(): - """Call onsuccess if all window (except main) are closed.""" - if self.allClosed(): - onsuccess() - else: - # still waiting for others to close - pass - if getattr(instance, "silentlyClose", False): instance.close() callback() diff --git a/aqt/addons.py b/aqt/addons.py index 70275bee3db..cd47506136f 100644 --- a/aqt/addons.py +++ b/aqt/addons.py @@ -901,6 +901,7 @@ def __hash__(self): """ Set of characteristic of Add-ons incorporated here""" incorporatedAddonsSet = { + Addon("Opening the same window multiple time", 354407385, 1545364194, "c832579f6ac7b327e16e6dfebcc513c1e89a693f", "https://github.com/Arthur-Milchior/anki-Multiple-Windows"), } incorporatedAddonsDict = {**{addon.name: addon for addon in incorporatedAddonsSet if addon.name}, diff --git a/aqt/preferences.py b/aqt/preferences.py index 32901cce08c..ce663cf17c6 100644 --- a/aqt/preferences.py +++ b/aqt/preferences.py @@ -271,8 +271,10 @@ def updateOneOption(self, name, default=False, check=True, sync=True): value = getattr(widget, function)() storeValue[name] = value - extraOptions = [ + extraOptions = ([ ] + +[(f"{window}MultipleTime", True) for window in ["AddCards", "EditCurrent", "Browser", "OtherWindows"]]#those are the windows name, thus with caps + ) def setupExtra(self): """Set in the GUI the preferences related to add-ons diff --git a/designer/preferences.ui b/designer/preferences.ui index 0009a7bf405..177ddbb4562 100644 --- a/designer/preferences.ui +++ b/designer/preferences.ui @@ -7,7 +7,7 @@ 0 0 423 - 508 + 552 @@ -489,6 +489,52 @@ + + + + Qt::Horizontal + + + + + + + <html><head/><body><p><span style=" font-weight:600;">Which window can be opened multiple time</span></p></body></html> + + + + + + + + + Add Cards + + + + + + + Browser + + + + + + + Edit Cards + + + + + + + Other + + + + + diff --git a/difference.md b/difference.md index eeaf6f0f9f3..ee0eda2fe2b 100644 --- a/difference.md +++ b/difference.md @@ -2,3 +2,9 @@ This files list the difference between regular anki and this forked version. It also lists the different options in the Preferences's extra page. +## Open a window multiple time (354407385) +Allows to open multiple copy of the same window. + +In the preferences, you can decide which you can open multiple time. + +TODO: do it better than using stacks From b7b7a0e08ed466b211316fed879509764c048e37 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Thu, 27 Jun 2019 05:57:58 +0200 Subject: [PATCH 48/68] Allow to postpone cards --- addons/1152543397/LICENSE | 674 ++++++++++++++++++++++++++++++++++ addons/1152543397/README.md | 104 ++++++ addons/1152543397/__init__.py | 74 ++++ addons/1152543397/ankiweb | 3 + addons/1152543397/config.json | 1 + addons/1152543397/config.md | 99 +++++ addons/1152543397/config.py | 27 ++ addons/1152543397/meta.json | 1 + anki/collection.py | 38 ++ aqt/addons.py | 1 + aqt/browser.py | 5 + aqt/main.py | 6 + aqt/preferences.py | 2 + designer/browser.ui | 8 +- designer/main.ui | 8 +- designer/preferences.ui | 71 +++- difference.md | 35 ++ 17 files changed, 1148 insertions(+), 9 deletions(-) create mode 100644 addons/1152543397/LICENSE create mode 100644 addons/1152543397/README.md create mode 100644 addons/1152543397/__init__.py create mode 100644 addons/1152543397/ankiweb create mode 100644 addons/1152543397/config.json create mode 100644 addons/1152543397/config.md create mode 100644 addons/1152543397/config.py create mode 100644 addons/1152543397/meta.json diff --git a/addons/1152543397/LICENSE b/addons/1152543397/LICENSE new file mode 100644 index 00000000000..f288702d2fa --- /dev/null +++ b/addons/1152543397/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/addons/1152543397/README.md b/addons/1152543397/README.md new file mode 100644 index 00000000000..d8abf56f4c8 --- /dev/null +++ b/addons/1152543397/README.md @@ -0,0 +1,104 @@ +# Postpone card's review +## Rationale +Some people find anki frustrating because it does not forgive you if +you skip a day or a week. When you come back, anki tells you to review +many days of cards immediatly. That's overwhelming, and sometime, it +makes people quit anki. + +While anki's right in a theoretical sens, it is wrong in a practical +sens. It may be better for the memory to see everything as soon as +possible. But in practice, it's even better to be late and not to quit +anki. + +This add-on is thus made for those people, who knows they are human, +and not perfect machine. Who knows that it's better to cheat with anki +a little bit than to quit it. This add-on change the due date of every +cards already learned once, and add to it as many day as you want. + +If you want to go one week in holiday, just add 7 days to every cards, +and you'll see the same number of cards as usual when you come +back. (Note however that you may find that you have forgotten more +cards than usual). + +### Second constraint +This add-on should also satisfies a second constraint. You don't need +it to review cards. + +You'll use this add-on to change the due date of cards. But once it's +done and you have synchronized your deck, you can see the cards on any +computer, phone, or using ankiweb. + +## Usage +You can either add delays to every cards or only to selected one. + +### All cards +In the main window, open ```tools``` then ```Postpone cards```. + +### Selected cards +In the browser, select every card you want to move. Select ```Edit```, +then select ```Postpone cards```, and the delay will only be +added to cards which you have selecteds and which are due. + +### Both cases +A window will ask you how many days of delay you want to +add. Enter the number, and press ok. + +The number can be either negative or positive. It means you can also +remove days. This may mostly be useful if you figure out you have +added to many days. + +The interval of each card is also changed, to reflect the change in +due date. See the configuration section below to learn more about it. + +## Configuration +The documentation for the configuration file can be found on +[config.md](https://github.com/Arthur-Milchior/Anki-postpone-reviews/config.md). + + +## Internal +This add-on does not change any method. Of course, it adds actions in +menus. + +## Version 2.0 +There are nothing similar in version 2.0 as far as I know + +## TODO +### Adding smaller intervals to cards due later +Let us assume you had spent a week without using anki. You use this +add-on and tell anki so that you don't have to immediatly see 7 cards. + +But, imagine: may be even if you don't want to see a full week of +cards in a single day, you'd accept to see 2 days of cards. It will +take twice as more time, but in a week, you won't be late anymore. + +If you set this option to 2, you'll see two days of card every day +during a week. + +In practice, it means that instead of adding 7 days to every single +cards, it will add 7 days to the cards due 7 days ago. It will add 6 +days to the card due 6 and 5 days ago. It will add 4 days to the cards +due 4 and 3 days ago, and so on. + +### Reorder in function of priority +This add-on does not take into account which cards are urgent and +which ones can wait. Maybe you took a week of holiday. During those +holidays, you were supposed to see cards which you last saw the week +before the holiday, and some other cards you last saw 6 months +ago. Clearly, the first one is more urgent than the second one. + +Using this option, you'll still have the same number of cards to see +today, but you'll see the more urgent one first. + +Not that the more urgent one may potentially be the hardest one. If +you use this option, you may have a lot of «see again» in the first +days, instead of having a few of them during the whole week. + +## Links, licence and credits + +Key |Value +------------|------------------------------------------------------------------- +Copyright | Arthur Milchior , Jasonbarc https://github.com/jasonsparc +Based on | Anki code by Damien Elmes +License | GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +Source in | https://github.com/Arthur-Milchior/Anki-postpone-reviews +Addon number| [1152543397](https://ankiweb.net/shared/info/1152543397) diff --git a/addons/1152543397/__init__.py b/addons/1152543397/__init__.py new file mode 100644 index 00000000000..6d2987e9fdf --- /dev/null +++ b/addons/1152543397/__init__.py @@ -0,0 +1,74 @@ +from anki.hooks import addHook +from aqt import mw +from .config import getIntervalCoefficient, getIntervalForNegativeCoefficient +from aqt.qt import QAction +from aqt.utils import getText, tooltip, showWarning +from anki.find import Finder + +#From https://stackoverflow.com/questions/1265665/how-can-i-check-if-a-string-represents-an-int-without-using-try-except +def RepresentsInt(s): + try: + return int(s) + except ValueError: + return None + +def getDelay(): + return getDelayWithResponse()[0] + +def getDelayWithResponse(): + (s, r) = getText("How many day to add to cards ? (negative number to substract days)") + if r: + return (RepresentsInt(s), r) + return (None, r) + +def getReviewCards(): + finder = Finder(mw.col) + cids = finder.findCards("is:review") + return cids + +def addDelay(cids): + (delay, delayResp) = getDelayWithResponse() + if delay is None: + if delayResp: + showWarning("Please enter an integral number of days") + return + + mw.checkpoint("Adding delay") + mw.progress.start() + + ivlDelay = max(0, round(delay * (getIntervalCoefficient() if delay >0 else getIntervalForNegativeCoefficient()))) + for cid in cids: + card = mw.col.getCard(cid) + if card.type !=2: + continue + card.ivl += ivlDelay + if card.odid: # Also update cards in filtered decks + card.odue += delay + else: + card.due += delay + card.flush() + + mw.progress.finish() + mw.col.reset() + mw.reset() + + tooltip(_("""Delay added.""")) + +def runMain(): + addDelay(getReviewCards()) + +def runBrowser(browser): + cids=browser.selectedCards() + addDelay(cids) + +text = "Postpone cards" +def setupBrowserMenu(browser): + a = QAction(text, browser) + a.triggered.connect(lambda : runBrowser(browser)) + browser.form.menuEdit.addAction(a) + +addHook("browser.setupMenus", setupBrowserMenu) +action = QAction(mw) +action.setText(text) +mw.form.menuTools.addAction(action) +action.triggered.connect(runMain) diff --git a/addons/1152543397/ankiweb b/addons/1152543397/ankiweb new file mode 100644 index 00000000000..fb86d314e83 --- /dev/null +++ b/addons/1152543397/ankiweb @@ -0,0 +1,3 @@ +Add some days to the due date of review notes. Thus, if you take a one-week holiday, you won't have 7 days of card to review when you come back. + +For more information, please go to https://github.com/Arthur-Milchior/anki-postpone-reviews \ No newline at end of file diff --git a/addons/1152543397/config.json b/addons/1152543397/config.json new file mode 100644 index 00000000000..23df2f06f9c --- /dev/null +++ b/addons/1152543397/config.json @@ -0,0 +1 @@ +{"interval coefficient": 0.33,"coefficient for negative": false} diff --git a/addons/1152543397/config.md b/addons/1152543397/config.md new file mode 100644 index 00000000000..f2f9ef1b447 --- /dev/null +++ b/addons/1152543397/config.md @@ -0,0 +1,99 @@ +## Changing the delay + +This configuration is slightly technical, you may probably ignore +it. + +In order to explain what it does, I need to explain some details about +anki's internal. I'll use an example. +#### Context + +Let's say you review a card the 31rst of December, and that this card +should be reviewd again 4 days later (i.e the 4th of January). We say +that the card's interval is 4 day. If you press «good» the 4th of +January, then the next interval will be 4*1.5=6 days, and you'll have +to see this card again the 10-th of january. (Here, 1.5 is an example, +the number may change). + +Now, let's imagine that, instead of reviewing the card the 4th, you +review this card the 10th of january. This card is two day late. The +question we have to consider is: what is the new interval ? Is it +still 4*1.5=6 days ? That makes no sens, if you successfully recalled +the cards after 10 days, there is no reason to use a 6 days interval +next. After all the real interval (i.e. theoretical inverval + +lateness) was already 10 days. + +Should the new interval be (4+6)*1.5 = 15 days ? Well, that may be an +overkill. If the card is really easy, ok. But if the card is "good", +15 days is still a big step for such a new card. So taking the whole +real interval into account is a bad idea. + +Thus, what anki does, is that: instead of using the theoretical +interval or the real interval it uses a ```planning interval``` whose +value is somewhere between the theoretical and the the real +interval. More precisely, this ```planning interval``` interval is the +sum of the theoretical interval and of the lateness, divided by 4 if +you pressed hard, by 2 if you pressed good, and by 1 if you pressed +easy. + +In our previous example, the new interval will be (4+6)*1.5 if you +press easy, (4+(6/2))*1.5=11 days if you press «good» and +(4+(6/4))*1.5=8 days if you press «hard». (Once again, its a +simplification. The value 1.5 is an example.) + +#### With this add-on +Now, instead of just seeing this card late, you decide to use this +add-on to add 6 days to every card. Thus, this card is considered to +be due the 10th of january. + +If you actually review it the 10-th of January, anki's will consider +that it's not late. I have no control over that. Because, you may see +this card on your phone, on ankiweb, on a computer where this add-on +is not installed... That's the second constraint mentionned above. + +What I can control is the value of the theoretical interval. I can +decide to add 6 days to it. Or only 6/2=3 days. That is, in order to +simulate anki's normal behavior, I'll have to guess in advance whether +you'll press hard, good or easy, to simulate it. + + +#### The configuration +The configuration parameter "interval coefficient" should be a +number. When you use the add-on stating that you want to add n days to +every cards, the theoretical interval will be incremented of +n*"interval coefficient" days. If this configuration is set to 0, the +theoretical interval is not changed. If this configuration is set to +1, then every days of delay is added as interval. + + +## The case of negative days + +There is a configuration "coefficient for negative". Its possible values +are: +* True: in which case the coefficient for positive number is also used + for negative +* False: in which case when a negative number of day is added, + intervals are note changed +* a number: in which case this number is added, as is in the positive + case. + +It is not clear whether changing the interval is a good idea in +general. + +### Why you may want to change intervals for negative number of days. + +If you added 10 days, and then remove 3 days, you probably want to +change the interval. So in the end, it will be like you directly added +7 days. (Note that because of rounding error, adding 10 and removing 3 +days is not exactly the same as directly adding 7 days) + +### Why you may want not to change intervals + +Imagine that you have added 7 days to all cards. +Then you see a new card. Let's say it now has an interval of four +days. + +If you decide to remove 7 days to every cards. This card will have its +interval decreased, and will have an interval of one. And there is +absolutly no reason to want it. The problem here is that this card was +new when you added days, and is not new anymore. Thus, only the second +action is applied to it. \ No newline at end of file diff --git a/addons/1152543397/config.py b/addons/1152543397/config.py new file mode 100644 index 00000000000..89987291992 --- /dev/null +++ b/addons/1152543397/config.py @@ -0,0 +1,27 @@ +from aqt import mw + +userOption = None +def getUserOption(): + global userOption + if userOption is None: + userOption = mw.addonManager.getConfig(__name__) + return userOption + +def update(_): + global userOption + userOption = None + +mw.addonManager.setConfigUpdatedAction(__name__,update) + +def getIntervalCoefficient(): + return getUserOption().get("interval coefficient", 0.33) + +def getIntervalForNegativeCoefficient(): + neg = getUserOption().get("coefficient for negative",False) + if neg is True: + return getIntervalCoefficient() + if neg is False: + return 0 + if isinstance(neg,(int,float)): + return neg + assert False diff --git a/addons/1152543397/meta.json b/addons/1152543397/meta.json new file mode 100644 index 00000000000..3eb5da9a323 --- /dev/null +++ b/addons/1152543397/meta.json @@ -0,0 +1 @@ +{"name": "Postpone cards review", "mod": 1560126139} \ No newline at end of file diff --git a/anki/collection.py b/anki/collection.py index cc0f7b94c49..8c196677d08 100644 --- a/anki/collection.py +++ b/anki/collection.py @@ -24,6 +24,7 @@ from anki.consts import * from anki.errors import AnkiError from anki.sound import stripSounds +from aqt.utils import showWarning, tooltip, getText import anki.latex # sets up hook import anki.cards import anki.notes @@ -664,6 +665,38 @@ def emptyCardReport(self, cids): c=ords, f=flds.replace("\x1f", " / ")) return rep + def addDelay(self, cids): + (delay, delayResp) = getText("How many day to add to cards ? (negative number to substract days)") + try: + delay = int(delay) + except ValueError: + showWarning("Please enter an integral number of days") + return None + if (not delayResp) or delay == 0: + return None + from aqt import mw + mw.checkpoint("Adding delay") + mw.progress.start() + ivlDelay = round(delay * (self.conf.get("factorAddDay", 0.33) if delay >0 else self.conf.get("factorRemoveDay", 0.33))) + for cid in cids: + card = mw.col.getCard(cid) + if card.type !=2: + continue + card.ivl += ivlDelay + if card.odid: # Also update cards in filtered decks + card.odue += delay + else: + card.due += delay + card.flush() + + mw.progress.finish() + mw.col.reset() + mw.reset() + + tooltip(_("""Delay added.""")) + + + # Field checksums and sorting fields ########################################################################## @@ -817,6 +850,11 @@ def findReplace(self, nids, src, dst, regex=None, field=None, fold=True): def findDupes(self, fieldName, search=""): return anki.find.findDupes(self, fieldName, search) + def getReviewCards(self): + finder = anki.find.Finder(self) + cardsToReview = finder.findCards("is:review") + return cardsToReview + # Stats ########################################################################## diff --git a/aqt/addons.py b/aqt/addons.py index 70275bee3db..740917905e7 100644 --- a/aqt/addons.py +++ b/aqt/addons.py @@ -901,6 +901,7 @@ def __hash__(self): """ Set of characteristic of Add-ons incorporated here""" incorporatedAddonsSet = { + Addon("Postpone cards review", 1152543397, 1560126139, "27103fd69c19e0576c5df6e28b5687a8a3e3d905", "https://github.com/Arthur-Milchior/Anki-postpone-reviews"), } incorporatedAddonsDict = {**{addon.name: addon for addon in incorporatedAddonsSet if addon.name}, diff --git a/aqt/browser.py b/aqt/browser.py index f0e74fef0a0..2ef5a31f7b3 100644 --- a/aqt/browser.py +++ b/aqt/browser.py @@ -458,6 +458,7 @@ def setupMenus(self): f.actionOrange_Flag.triggered.connect(lambda: self.onSetFlag(2)) f.actionGreen_Flag.triggered.connect(lambda: self.onSetFlag(3)) f.actionBlue_Flag.triggered.connect(lambda: self.onSetFlag(4)) + f.actionPostpone_reviews.triggered.connect(self.onPostpone_reviews) # jumps f.actionPreviousCard.triggered.connect(self.onPreviousCard) f.actionNextCard.triggered.connect(self.onNextCard) @@ -1702,6 +1703,10 @@ def _reschedule(self): self.mw.requireReset() self.model.endReset() + def onPostpone_reviews(self): + cids=self.selectedCards() + self.col.addDelay(cids) + # Edit: selection ###################################################################### diff --git a/aqt/main.py b/aqt/main.py index 4525ba78078..2f99d940178 100644 --- a/aqt/main.py +++ b/aqt/main.py @@ -23,6 +23,7 @@ import aqt.toolbar import aqt.stats import aqt.mediasrv +from anki.find import Finder import anki.sound import anki.mpv from aqt.utils import saveGeom, restoreGeom, showInfo, showWarning, \ @@ -955,6 +956,10 @@ def onAbout(self): """Open the about window""" aqt.dialogs.open("About", self) + def onPostpone_Reviews(self): + """Open the about window""" + self.col.addDelay(self.col.getReviewCards()) + def onDonate(self): """Ask the OS to open the donate web page""" openLink(aqt.appDonate) @@ -1014,6 +1019,7 @@ def setupMenus(self): m.actionExport.triggered.connect(self.onExport) m.actionExit.triggered.connect(self.close) m.actionPreferences.triggered.connect(self.onPrefs) + m.actionPostpone_Reviews.triggered.connect(self.onPostpone_Reviews) m.actionAbout.triggered.connect(self.onAbout) m.actionUndo.triggered.connect(self.onUndo) if qtminor < 11: diff --git a/aqt/preferences.py b/aqt/preferences.py index 32901cce08c..fb028df9350 100644 --- a/aqt/preferences.py +++ b/aqt/preferences.py @@ -272,6 +272,8 @@ def updateOneOption(self, name, default=False, check=True, sync=True): storeValue[name] = value extraOptions = [ + ("factorAddDay", 0.33, False), + ("factorRemoveDay", 0.33, False), ] def setupExtra(self): diff --git a/designer/browser.ui b/designer/browser.ui index 71ddbf188e3..7460bbae24b 100644 --- a/designer/browser.ui +++ b/designer/browser.ui @@ -236,7 +236,7 @@ 0 0 750 - 22 + 20 @@ -295,6 +295,7 @@ + @@ -599,6 +600,11 @@ Ctrl+K + + + P&ostpone reviews + + diff --git a/designer/main.ui b/designer/main.ui index 654bda94e10..eca173a4515 100644 --- a/designer/main.ui +++ b/designer/main.ui @@ -46,7 +46,7 @@ 0 0 412 - 22 + 20 @@ -90,6 +90,7 @@ + @@ -237,6 +238,11 @@ Ctrl+Shift+A + + + P&ostpone Reviews + + diff --git a/designer/preferences.ui b/designer/preferences.ui index 0009a7bf405..490181526df 100644 --- a/designer/preferences.ui +++ b/designer/preferences.ui @@ -491,16 +491,73 @@ + + + - Qt::Vertical + Qt::Horizontal + + + + + + + When you post pone cards, how much should anki take into account the fact this postponing to compute the new delays ? - - - 20 - 40 - + + false + + + true + + + + + + + QFormLayout::WrapLongRows - + + + + When adding days + + + + + + + 1.000000000000000 + + + 0.100000000000000 + + + 0.330000000000000 + + + + + + + When removing days + + + + + + + 1.000000000000000 + + + 0.100000000000000 + + + 0.330000000000000 + + + + diff --git a/difference.md b/difference.md index eeaf6f0f9f3..18db7debf0f 100644 --- a/difference.md +++ b/difference.md @@ -2,3 +2,38 @@ This files list the difference between regular anki and this forked version. It also lists the different options in the Preferences's extra page. +## Postpone reviews (1152543397) + +In the main window «Tool>Postpone reviews» allow you to tell anki that +you'll go (or have been) in holiday during a week, and so Anki should +add 7 days to every cards which are due. So, back from holiday, you'll +find anki as if you never left. + +In the browser, you can select cards, then do «Cards>Postpone reviews» +to apply the same effect to selected cards only. + +WARNING: This is a really bad idea. Because, you will see a lot of +cards too late and forget them. However, if you feel that, without +this feature, you'll just quit, then it's still better using it. + +Note that the number of day could be negative. If you postpone review +by -1 day, then you'll see today tomorrow's card. + +### Configuration + +When you are late, this is taken into account by anki. For example, if +a card has a delay of two days, you take a week of vacation, and you +succesfully review the card, anki will remark that you recalled the +card seven days after last seeing it. Thus, it won't consider that the +delay is two days, but that it is seven days. And thus it will +computer a new delays of fourteen days, may be. + +If you want to have this effect fully taken into account while using +this feature, in the preferences>extra, set «When adding days» +to 1. If you want that Anki totally ignores the fact that you have +added some days, and that it considers that the interval was two days, +then set this number to 0. If you want that anki considers this +postponing, but not totally, set a number between 0 and 1. + +The «When removing days» feature is similar to the «When adding days», +but consider days removed. From 463552aee2dc40efc265bd678575fc6ad5830806 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Mon, 17 Jun 2019 14:29:37 +0200 Subject: [PATCH 49/68] Quicker rendering of QA --- addons/115825506/LICENSE | 674 +++++++++++++++++++++++++++++++++++ addons/115825506/README.md | 60 ++++ addons/115825506/__init__.py | 2 + addons/115825506/meta.json | 1 + addons/115825506/render.py | 62 ++++ addons/115825506/timer.py | 137 +++++++ anki/template/template.py | 95 +++-- aqt/addons.py | 1 + difference.md | 3 + 9 files changed, 981 insertions(+), 54 deletions(-) create mode 100755 addons/115825506/LICENSE create mode 100755 addons/115825506/README.md create mode 100755 addons/115825506/__init__.py create mode 100755 addons/115825506/meta.json create mode 100755 addons/115825506/render.py create mode 100755 addons/115825506/timer.py diff --git a/addons/115825506/LICENSE b/addons/115825506/LICENSE new file mode 100755 index 00000000000..f288702d2fa --- /dev/null +++ b/addons/115825506/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/addons/115825506/README.md b/addons/115825506/README.md new file mode 100755 index 00000000000..46b63fabda9 --- /dev/null +++ b/addons/115825506/README.md @@ -0,0 +1,60 @@ +# Improve rendering +Changing a model may be a really long process. This can be partially +solved using add-on ()[] (full disclosure: I also wrote it). As an +example, I've got a note type with more than a hundred field and card +type. Changing it without this add-on takes 7 minutes and a half while +using this add-on, it takes only 3 minutes. + +## Warning +This change the result of the card generated in a few case. Most of +them should either be improvement, or not occur in normal use of anki. + +### Field name inside field. +In a basic card, if you write "{{Back}}" in the Front field, you'll +see the content of the back field where you should see the front +field. This is because {{Back}} is found in the generated card and +anki assumes it must be replaced by the value of the Back field. + +This does not occur anymore. This add-ons ensure anki does all +replacement in a single pass, instead of doing it iteratively. On a +technical side, this is what allow the running time to be linear in +the number of field instance instead of quadratic (product of length +and of number of distinct field). + +### More than a hundred field. +By default, anki allows at most a hundred field by card. This add-on +remove this restriction. Thanks to the time improvement, this is +actually not an important limit anymore. + +### Badly formed card type +A template such as +```python +{{#Front}}{{#Back}} {{Front}}{{Back}}{{/Front}} {{/Back}} +``` +is theoretically invalid. Indeed, you should cloze Back before Front +since you opened it after Front. If you create a note with some value +in Back and no value in Front, you'll see the message "{unknown field +/Back}" (which is a really badly written error message). However, +anki will still generate some content when you use this model. It is possible that the content +generated with and without this add-on will differ. + +## Internal +This modify the following method, without calling the previous +version. + +* anki.template.template.Template.render_sections +* anki.template.template.Template.render_tags +It also creates the methods: +* anki.template.template.Template.sub_section +* anki.template.template.Template.sub_tag + +## Links, licence and credits + +Key |Value +------------|------------------------------------------------------------------- +Copyright | Arthur Milchior +Based on | Anki code by Damien Elmes +License | GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +Source in | https://github.com/Arthur-Milchior/anki-better-card-generation +Addon number| [115825506](https://ankiweb.net/shared/info/115825506) +Support me on| [![Ko-fi](https://ko-fi.com/img/Kofi_Logo_Blue.svg)](Ko-fi.com/arthurmilchior) or [![Patreon](http://www.milchior.fr/patreon.png)](https://www.patreon.com/bePatron?u=146206) diff --git a/addons/115825506/__init__.py b/addons/115825506/__init__.py new file mode 100755 index 00000000000..0730ec67837 --- /dev/null +++ b/addons/115825506/__init__.py @@ -0,0 +1,2 @@ +from . import render +#from . import timer diff --git a/addons/115825506/meta.json b/addons/115825506/meta.json new file mode 100755 index 00000000000..0fa2d405dbb --- /dev/null +++ b/addons/115825506/meta.json @@ -0,0 +1 @@ +{"name": "Improve speed of change of note type", "mod": 1551823299} \ No newline at end of file diff --git a/addons/115825506/render.py b/addons/115825506/render.py new file mode 100755 index 00000000000..14fa9c90431 --- /dev/null +++ b/addons/115825506/render.py @@ -0,0 +1,62 @@ +from anki.template.template import Template, get_or_attr, modifiers +from anki.utils import stripHTMLMedia +import re + +def sub_section(self, match, context): + section, section_name, inner = match.group(0, 1, 2) + section_name = section_name.strip() + + # val will contain the content of the field considered + # right now + val = None + m = re.match("c[qa]:(\d+):(.+)", section_name) + if m: + # get full field text + txt = get_or_attr(context, m.group(2), None) + m = re.search(clozeReg%m.group(1), txt) + if m: + val = m.group(1) + else: + val = get_or_attr(context, section_name, None) + replacer = '' + # Whether it's {{^ + inverted = section[2] == "^" + # Ensuring we don't consider whitespace in wval + if val: + val = stripHTMLMedia(val).strip() + if (val and not inverted) or (not val and inverted): + replacer = inner + return replacer +Template.sub_section = sub_section + +def render_sections(self, template, context): + """replace {{#foo}}bar{{/foo}} and {{^foo}}bar{{/foo}} by + their normal value.""" + n = 1 + while n: + template, n = self.section_re.subn(lambda match:self.sub_section(match,context), template) + return template + +Template.render_sections = render_sections + + +def sub_tag(self, match, context): + tag, tag_type, tag_name = match.group(0, 1, 2) + # i.e. "{{!foo}}", "!", "foo" + tag_name = tag_name.strip() + func = modifiers[tag_type] + replacement = func(self, tag_name, context) + return replacement +Template.sub_tag = sub_tag + +def render_tags(self, template, context): + """Renders all the tags in a template for a context. Normally + {{# and {{^ are already removed.""" + try: + return self.tag_re.sub(lambda match: self.sub_tag(match, context),template) + except (SyntaxError, KeyError): + return "{{invalid template}}" + + + +Template.render_tags = render_tags diff --git a/addons/115825506/timer.py b/addons/115825506/timer.py new file mode 100755 index 00000000000..7da2bf765d4 --- /dev/null +++ b/addons/115825506/timer.py @@ -0,0 +1,137 @@ +# from anki.template.template import Template +import time +import anki +from anki.models import ModelManager +from anki.sound import stripSounds +from anki.utils import splitFields +from anki.consts import * +import re +from anki.hooks import runFilter + +# oldRender = Template.render +# def render(*args,**kwargs): +# start = time.clock() +# end = time.clock() +# ret = oldRender(*args,**kwargs) +# dif = end-start +# print(f"Render took {dif} seconds") +# return ret + +# Template.render = render + +from anki.collection import _Collection + +old_updateRequired = ModelManager._updateRequired +def _updateRequired(*args,**kwargs): + start = time.clock() + ret = old_updateRequired(*args,**kwargs) + end = time.clock() + dif = end-start + print(f"_updateRequired took {dif} seconds") + return ret +ModelManager._updateRequired = _updateRequired + +def renderQA(self, ids=None, type="card"): + # gather metadata + """TODO + + The list of renderQA for each cards whose type belongs to ids. + + Types may be card(default), note, model or all (in this case, ids is not used). + It seems to be called nowhere + """ + if type == "card": + where = "and c.id in " + ids2str(ids) + elif type == "note": + where = "and f.id in " + ids2str(ids) + elif type == "model": + where = "and m.id in " + ids2str(ids) + elif type == "all": + where = "" + else: + raise Exception() + start = time.clock() + t= [self._renderQA(row) + for row in self._qaData(where)] + end = time.clock() + dif = end-start + print(f"Renders took {dif} seconds") + return t +#_Collection.renderQA = renderQA + +def _renderQA(self, data, qfmt=None, afmt=None): + """Returns hash of id, question, answer. + + Keyword arguments: + data -- [cid, nid, mid, did, ord, tags, flds] (see db + documentation for more information about those values) + This corresponds to the information you can obtain in templates, using {{Tags}}, {{Type}}, etc.. + qfmt -- question format string (as in template) + afmt -- answer format string (as in template) + + unpack fields and create dict + TODO comment better + + """ + cid, nid, mid, did, ord, tags, flds, cardFlags = data + flist = splitFields(flds)#the list of fields + fields = {} # + #name -> ord for each field, tags + # Type: the name of the model, + # Deck, Subdeck: their name + # Card: the template name + # cn: 1 for n being the ord+1 + # FrontSide : + model = self.models.get(mid) + for (name, (idx, conf)) in list(self.models.fieldMap(model).items()):#conf is not used + fields[name] = flist[idx] + fields['Tags'] = tags.strip() + fields['Type'] = model['name'] + fields['Deck'] = self.decks.name(did) + fields['Subdeck'] = fields['Deck'].split('::')[-1] + if model['type'] == MODEL_STD:#Note that model['type'] has not the same meaning as fields['Type'] + template = model['tmpls'][ord] + else:#for cloze deletions + template = model['tmpls'][0] + fields['Card'] = template['name'] + fields['c%d' % (ord+1)] = "1" + # render q & a + d = dict(id=cid) + # id: card id + qfmt = qfmt or template['qfmt'] + afmt = afmt or template['afmt'] + start = time.clock() + for (type, format) in (("q", qfmt), ("a", afmt)): + if type == "q": + format = re.sub("{{(?!type:)(.*?)cloze:", r"{{\1cq-%d:" % (ord+1), format) + #Replace {{'foo'cloze: by {{'foo'cq-(ord+1), where 'foo' does not begins with "type:" + format = format.replace("<%cloze:", "<%%cq:%d:" % ( + ord+1)) + #Replace <%cloze: by <%%cq:(ord+1) + else: + format = re.sub("{{(.*?)cloze:", r"{{\1ca-%d:" % (ord+1), format) + #Replace {{'foo'cloze: by {{'foo'ca-(ord+1) + format = format.replace("<%cloze:", "<%%ca:%d:" % ( + ord+1)) + #Replace <%cloze: by <%%ca:(ord+1) + fields['FrontSide'] = stripSounds(d['q']) + #d['q'] is defined during loop's first iteration + fields = runFilter("mungeFields", fields, model, data, self) # TODO check + html = anki.template.render(format, fields) #replace everything of the form {{ by its value TODO check + d[type] = runFilter( + "mungeQA", html, type, fields, model, data, self) # TODO check + # empty cloze? + if type == 'q' and model['type'] == MODEL_CLOZE: + if not self.models._availClozeOrds(model, flds, False): + d['q'] += ("

" + _( + "Please edit this note and add some cloze deletions. (%s)") % ( + "%s" % (HELP_SITE, _("help")))) + #in the case where there is a cloze note type + #without {{cn in fields indicated by + #{{cloze:fieldName; an error message should be + #shown + end = time.clock() + dif = end-start + print(f"Renders took {dif} seconds") + return d +#_Collection._renderQA = _renderQA diff --git a/anki/template/template.py b/anki/template/template.py index ea83e3be361..53cc2edf898 100644 --- a/anki/template/template.py +++ b/anki/template/template.py @@ -96,69 +96,56 @@ def compile_regexps(self): tag = r"%(otag)s(#|=|&|!|>|\{)?(.+?)\1?%(ctag)s+" self.tag_re = re.compile(tag % tags) - def render_sections(self, template, context): - """replace {{#foo}}bar{{/foo}} and {{^foo}}bar{{/foo}} by - their normal value.""" - while 1: - match = self.section_re.search(template) - if match is None: - break - - section, section_name, inner = match.group(0, 1, 2) - section_name = section_name.strip() - - # val will contain the content of the field considered - # right now - val = None - m = re.match(r"c[qa]:(\d+):(.+)", section_name) + def sub_section(self, match, context): + section, section_name, inner = match.group(0, 1, 2) + section_name = section_name.strip() + + # val will contain the content of the field considered + # right now + val = None + m = re.match("c[qa]:(\d+):(.+)", section_name) + if m: + # get full field text + txt = get_or_attr(context, m.group(2), None) + m = re.search(clozeReg%m.group(1), txt) if m: - # get full field text - txt = get_or_attr(context, m.group(2), None) - m = re.search(clozeReg%m.group(1), txt) - if m: - val = m.group(1) - else: - val = get_or_attr(context, section_name, None) - - replacer = '' - # Whether it's {{^ - inverted = section[2] == "^" - # Ensuring we don't consider whitespace in wval - if val: - val = stripHTMLMedia(val).strip() - if (val and not inverted) or (not val and inverted): + val = m.group(1) + else: + val = get_or_attr(context, section_name, None) + replacer = '' + # Whether it's {{^ + inverted = section[2] == "^" + # Ensuring we don't consider whitespace in wval + if val: + val = stripHTMLMedia(val).strip() + if (val and not inverted) or (not val and inverted): replacer = inner + return replacer - template = template.replace(section, replacer) - + def render_sections(self, template, context): + """replace {{#foo}}bar{{/foo}} and {{^foo}}bar{{/foo}} by + their normal value.""" + n = 1 + while n: + template, n = self.section_re.subn(lambda match:self.sub_section(match,context), template) return template + def sub_tag(self, match, context): + tag, tag_type, tag_name = match.group(0, 1, 2) + # i.e. "{{!foo}}", "!", "foo" + tag_name = tag_name.strip() + func = modifiers[tag_type] + replacement = func(self, tag_name, context) + return replacement + def render_tags(self, template, context): """Renders all the tags in a template for a context. Normally {{# and {{^ are already removed.""" repCount = 0 - while 1: - if repCount > 100: - print("too many replacements") - break - repCount += 1 - - # search for some {{foo}} - match = self.tag_re.search(template) - if match is None: - break - - # - tag, tag_type, tag_name = match.group(0, 1, 2) - tag_name = tag_name.strip() - try: - func = modifiers[tag_type] - replacement = func(self, tag_name, context) - template = template.replace(tag, replacement) - except (SyntaxError, KeyError): - return "{{invalid template}}" - - return template + try: + return self.tag_re.sub(lambda match: self.sub_tag(match, context),template) + except (SyntaxError, KeyError): + return "{{invalid template}}" # {{{ functions just like {{ in anki @modifier('{') diff --git a/aqt/addons.py b/aqt/addons.py index 70275bee3db..692e4cc76d3 100644 --- a/aqt/addons.py +++ b/aqt/addons.py @@ -901,6 +901,7 @@ def __hash__(self): """ Set of characteristic of Add-ons incorporated here""" incorporatedAddonsSet = { + Addon("Improve speed of change of note type", 115825506, 1551823299, "8b125aa55a490b276019d1a2e7f6f8c0767d65b3", "https://github.com/Arthur-Milchior/anki-better-card-generation"), } incorporatedAddonsDict = {**{addon.name: addon for addon in incorporatedAddonsSet if addon.name}, diff --git a/difference.md b/difference.md index eeaf6f0f9f3..bcf9669364a 100644 --- a/difference.md +++ b/difference.md @@ -2,3 +2,6 @@ This files list the difference between regular anki and this forked version. It also lists the different options in the Preferences's extra page. +## Improve rendering (1551823299) +Anki will generate cards's content (question, answer) faster. It will +also improve the speed at which anki will save modification of card type. From 7bc67050f7e1613ec0f4f41703558d0e91f29b51 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Mon, 17 Jun 2019 08:15:28 +0200 Subject: [PATCH 50/68] Tag missing media --- addons/2027876532/README.md | 33 +++++++++ addons/2027876532/__init__.py | 122 ++++++++++++++++++++++++++++++++++ addons/2027876532/meta.json | 1 + anki/media.py | 39 ++++++++++- aqt/addons.py | 1 + aqt/main.py | 4 +- aqt/preferences.py | 1 + designer/preferences.ui | 7 ++ difference.md | 7 ++ tests/test_media.py | 2 +- 10 files changed, 212 insertions(+), 5 deletions(-) create mode 100644 addons/2027876532/README.md create mode 100644 addons/2027876532/__init__.py create mode 100644 addons/2027876532/meta.json diff --git a/addons/2027876532/README.md b/addons/2027876532/README.md new file mode 100644 index 00000000000..b3022d312ba --- /dev/null +++ b/addons/2027876532/README.md @@ -0,0 +1,33 @@ +# Tag missing media +## Rationale +When you click on «check media», anki gives the list of missing +media. I find that it does not help a lot to correct them. Indeed, I +would need to check by hand each missing media in the browser. + +This add-on ensures that, when «check media» is used, it adds the tag +"MissingMedia" to each note with a missing media and open the browser +with the notes which have a missing media. + +## Warning +check media also removes the tag «MissingMedia» from notes which does +not have a missing media. So that if you have corrected a note, it +does not appear anymore when you search for notes with Missing Media. +## Internal +It totally redefines the method `anki.media.MediaManager.check`. So +this add-on is incompatible with any other add-on changing this +method. + +## Version 2.0 +None + + +## Links, licence and credits + +Key |Value +------------|------------------------------------------------------------------- +Copyright | Arthur Milchior +Based on | Anki code by Damien Elmes +License | GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +Source in | https://github.com/Arthur-Milchior/anki-tag-missing-medias +Addon number| [2027876532](https://ankiweb.net/shared/info/2027876532) +Support me on| [![Ko-fi](https://ko-fi.com/img/Kofi_Logo_Blue.svg)](Ko-fi.com/arthurmilchior) or [![Patreon](http://www.milchior.fr/patreon.png)](https://www.patreon.com/bePatron?u=146206) diff --git a/addons/2027876532/__init__.py b/addons/2027876532/__init__.py new file mode 100644 index 00000000000..f999e31aa54 --- /dev/null +++ b/addons/2027876532/__init__.py @@ -0,0 +1,122 @@ +from anki.media import * +from anki.notes import Note +from aqt import mw, dialogs +from anki.find import Finder + +# oldCheck = MediaManager.check +# def check(self, local=None): +# (missingFiles, unusedFiles) = oldCheck(self, local) +# nids = set() +# query = """select id from notes where flds like "" """ + +def check(self, local=None): + "Return (missingFiles, unusedFiles)." + mdir = self.dir() + # gather all media references in NFC form + allRefs = set() + refsToNid = dict() # this dic is new + for nid, mid, flds in self.col.db.execute("select id, mid, flds from notes"): + noteRefs = self.filesInStr(mid, flds) + # check the refs are in NFC + for f in noteRefs: + # if they're not, we'll need to fix them first + if f != unicodedata.normalize("NFC", f): + self._normalizeNoteRefs(nid) + noteRefs = self.filesInStr(mid, flds) + break + # new. update refsToNid + for f in noteRefs: + if f not in refsToNid: + refsToNid[f] = set() + refsToNid[f].add(nid) + # end new + allRefs.update(noteRefs) + # loop through media folder + unused = [] + if local is None: + files = os.listdir(mdir) + else: + files = local + renamedFiles = False + dirFound = False + warnings = [] + for file in files: + if not local: + if not os.path.isfile(file): + # ignore directories + dirFound = True + continue + if file.startswith("_"): + # leading _ says to ignore file + continue + + if self.hasIllegal(file): + name = file.encode(sys.getfilesystemencoding(), errors="replace") + name = str(name, sys.getfilesystemencoding()) + warnings.append( + _("Invalid file name, please rename: %s") % name) + continue + + nfcFile = unicodedata.normalize("NFC", file) + # we enforce NFC fs encoding on non-macs + if not isMac and not local: + if file != nfcFile: + # delete if we already have the NFC form, otherwise rename + if os.path.exists(nfcFile): + os.unlink(file) + renamedFiles = True + else: + os.rename(file, nfcFile) + renamedFiles = True + file = nfcFile + # compare + if nfcFile not in allRefs: + unused.append(file) + else: + allRefs.discard(nfcFile) + # if we renamed any files to nfc format, we must rerun the check + # to make sure the renamed files are not marked as unused + if renamedFiles: + return self.check(local=local) + # NEW: A line here removed because it was a bug + # New + finder = Finder(mw.col) + alreadyMissingNids = finder.findNotes("tag:MissingMedia") + nidsOfMissingRefs = set() + for ref in allRefs: + nidsOfMissingRefs.update(refsToNid[ref]) + #print(f"nidsOfMissingRefs is now {nidsOfMissingRefs}") + + for nid in nidsOfMissingRefs: + if nid not in alreadyMissingNids: + print(f"missing nid {nid}") + note = Note(mw.col, id = nid) + note.addTag("MissingMedia") + note.flush() + + for nid in alreadyMissingNids: + if nid not in nidsOfMissingRefs: + print(f"not missing anymore nid {nid}") + note = Note(mw.col, id = nid) + note.delTag("MissingMedia") + note.flush() + # end new + + # make sure the media DB is valid + try: + self.findChanges() + except DBError: + self._deleteDB() + if dirFound: + warnings.append( + _("Anki does not support files in subfolders of the collection.media folder.")) + if allRefs: + browser = dialogs.open("Browser", mw) + browser.form.searchEdit.lineEdit().setText("tag:MissingMedia") + browser.onSearchActivated() + return (allRefs, unused, warnings) + + + +MediaManager.check = check +print("Missing Media imported") diff --git a/addons/2027876532/meta.json b/addons/2027876532/meta.json new file mode 100644 index 00000000000..d7b38240853 --- /dev/null +++ b/addons/2027876532/meta.json @@ -0,0 +1 @@ +{"name": "Add a tag to notes with missing media", "mod": 1560318502} \ No newline at end of file diff --git a/anki/media.py b/anki/media.py index 5377af61974..dd0a2ad043a 100644 --- a/anki/media.py +++ b/anki/media.py @@ -27,11 +27,14 @@ import json import os + +from anki.find import Finder from anki.utils import checksum, isWin, isMac from anki.db import DB, DBError from anki.consts import * from anki.latex import mungeQA from anki.lang import _ +from anki.notes import Note class MediaManager: """ @@ -239,7 +242,7 @@ def repl(match): ########################################################################## def filesInStr(self, mid, string, includeRemote=False): - """The list of media's path in the string. + """The list of media's path in the string. Medias starting with _ are treated as any media. @@ -345,6 +348,7 @@ def check(self, local=None): mdir = self.dir() # gather all media references in NFC form allRefs = set() + refsToNid = dict() # this dic is new for nid, mid, flds in self.col.db.execute("select id, mid, flds from notes"): noteRefs = self.filesInStr(mid, flds) # check the refs are in NFC @@ -354,6 +358,11 @@ def check(self, local=None): self._normalizeNoteRefs(nid) noteRefs = self.filesInStr(mid, flds) break + # Compute the list of notes with missing media + for f in noteRefs: + if f not in refsToNid: + refsToNid[f] = set() + refsToNid[f].add(nid) allRefs.update(noteRefs) # loop through media folder unused = [] @@ -403,6 +412,27 @@ def check(self, local=None): if renamedFiles: return self.check(local=local) nohave = [x for x in allRefs if not x.startswith("_")] + + # Deal with tag + finder = Finder(self.col) + alreadyMissingNids = finder.findNotes("tag:MissingMedia") + nidsOfMissingRefs = set() + for ref in allRefs: + nidsOfMissingRefs.update(refsToNid[ref]) + + # remove tags when a note has no missing media anymore + for nid in nidsOfMissingRefs: + if nid not in alreadyMissingNids: + note = Note(self.col, id = nid) + note.addTag("MissingMedia") + note.flush() + + # Add tags to notes with missing media + for nid in alreadyMissingNids: + if nid not in nidsOfMissingRefs: + note = Note(self.col, id = nid) + note.delTag("MissingMedia") + note.flush() # make sure the media DB is valid try: self.findChanges() @@ -412,7 +442,12 @@ def check(self, local=None): if dirFound: warnings.append( _("Anki does not support files in subfolders of the collection.media folder.")) - return (nohave, unused, warnings) + from aqt import mw, dialogs #TODO Try to avoid using import here if possible. + if allRefs and mw and self.col.conf.get("browserOnMissingMedia", True): # open browser with missing medias + browser = dialogs.open("Browser", mw) + browser.form.searchEdit.lineEdit().setText("tag:MissingMedia") + browser.onSearchActivated() + return (allRefs, unused, warnings) def _normalizeNoteRefs(self, nid): note = self.col.getNote(nid) diff --git a/aqt/addons.py b/aqt/addons.py index 70275bee3db..da0c30873ca 100644 --- a/aqt/addons.py +++ b/aqt/addons.py @@ -901,6 +901,7 @@ def __hash__(self): """ Set of characteristic of Add-ons incorporated here""" incorporatedAddonsSet = { + Addon("Add a tag to notes with missing media", 2027876532, 1560318502, "26c4f6158ce2b8811b8ac600ed8a0204f5934d0b", "Arthur-Milchior/anki-tag-missing-medias"), } incorporatedAddonsDict = {**{addon.name: addon for addon in incorporatedAddonsSet if addon.name}, diff --git a/aqt/main.py b/aqt/main.py index 4525ba78078..d2e459cf032 100644 --- a/aqt/main.py +++ b/aqt/main.py @@ -1174,9 +1174,9 @@ def onCheckDB(self): return ret def onCheckMediaDB(self): - self.progress.start(immediate=True) + #self.progress.start(immediate=True)#TODO: understand why finish raise an exception (nohave, unused, warnings) = self.col.media.check() - self.progress.finish() + #self.progress.finish() # generate report report = "" if warnings: diff --git a/aqt/preferences.py b/aqt/preferences.py index 32901cce08c..a4a9c2eaeef 100644 --- a/aqt/preferences.py +++ b/aqt/preferences.py @@ -272,6 +272,7 @@ def updateOneOption(self, name, default=False, check=True, sync=True): storeValue[name] = value extraOptions = [ + ("browserOnMissingMedia", True), ] def setupExtra(self): diff --git a/designer/preferences.ui b/designer/preferences.ui index 0009a7bf405..54bdb3c8862 100644 --- a/designer/preferences.ui +++ b/designer/preferences.ui @@ -489,6 +489,13 @@ + + + + In case of missing media, show the notes in the browser. + + + diff --git a/difference.md b/difference.md index eeaf6f0f9f3..9bb4e0acb65 100644 --- a/difference.md +++ b/difference.md @@ -2,3 +2,10 @@ This files list the difference between regular anki and this forked version. It also lists the different options in the Preferences's extra page. +## Tag missing media (2027876532) +If a note is supposed to have media (image or audio), it will have the +tag "MissingMedia", when you «check media». + +The prefenece "In case of missing media, show the notes in the +browser" allows to decide whether the browser show you the list of +notes with missing media when you check media. diff --git a/tests/test_media.py b/tests/test_media.py index e8198a543ef..8e6907236a4 100644 --- a/tests/test_media.py +++ b/tests/test_media.py @@ -67,7 +67,7 @@ def test_deckIntegration(): f.write("test") # check media ret = d.media.check() - assert ret[0] == ["fake2.png"] + assert ret[0] == {"fake2.png"} assert ret[1] == ["foo.jpg"] def test_changes(): From 082766869fc2790c091720a50534e4ba21201543 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Sat, 6 Jul 2019 08:12:39 +0200 Subject: [PATCH 51/68] Allow complex card type --- addons/1713990897/LICENSE | 674 ++++++++++++++++++++++++++++++++++ addons/1713990897/README.md | 45 +++ addons/1713990897/__init__.py | 1 + addons/1713990897/ankiweb | 3 + addons/1713990897/debug.py | 49 +++ addons/1713990897/init.py | 222 +++++++++++ addons/1713990897/meta.json | 1 + addons/1713990897/zipping | 2 + anki/cards.py | 4 + anki/collection.py | 5 +- anki/models.py | 32 ++ anki/template/__init__.py | 5 +- anki/template/template.py | 33 +- aqt/addons.py | 1 + aqt/preferences.py | 1 + designer/preferences.ui | 30 +- difference.md | 6 + tests/test_models.py | 10 +- 18 files changed, 1106 insertions(+), 18 deletions(-) create mode 100644 addons/1713990897/LICENSE create mode 100644 addons/1713990897/README.md create mode 100644 addons/1713990897/__init__.py create mode 100644 addons/1713990897/ankiweb create mode 100644 addons/1713990897/debug.py create mode 100644 addons/1713990897/init.py create mode 100644 addons/1713990897/meta.json create mode 100755 addons/1713990897/zipping diff --git a/addons/1713990897/LICENSE b/addons/1713990897/LICENSE new file mode 100644 index 00000000000..f288702d2fa --- /dev/null +++ b/addons/1713990897/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/addons/1713990897/README.md b/addons/1713990897/README.md new file mode 100644 index 00000000000..1295fd3b70a --- /dev/null +++ b/addons/1713990897/README.md @@ -0,0 +1,45 @@ +# Allowing complex card template +## Rationale +If you are a power user, you may want to be able to use complex +template. The rule «a card is generated if and only if the question +side show the content of a field» is a really nice rule. But that's +far from being what anki actually does. See [generation rules](https://github.com/Arthur-Milchior/anki/blob/master/documentation/templates_generation_rules.md) +for a real explanation + +So, this add-on change anki, in order to ensure that cards are +generated if only if they should be generated ! + + +## Warning +It should sadly be noted that this add-on is kind of incompatible with +any version of anki without this addon. It means that some cards may +be generated here, and be seen as empty on ankidroid/ankiweb/ios. + + +## Internal +It changes the following methods: +* ```Template.render_unescaped```, it now returns a pair, whose second + element is True if and only if the field was found and + showAField. (Currently, the field may be a special field. TODO: remove this) +* ```Template.render_tags```: similarly, it returns a pair, which is + true if only if some field was found and showAField. +* ```Template.render```: same modification +* ```_Collection._renderQA```: The dictionnary returned contains an + entry "showAField" which has the meaning of the previous change +* ```Card.isEmpty```: it now returns the correct answer ! +* ```ModelManager.availOrds```: Same thing + +## Version 2.0 +None +## TODO +Do not consider cards with only special fields as successful cards. + +## Links, licence and credits + +Key |Value +------------|------------------------------------------------------------------- +Copyright | Arthur Milchior +Based on | Anki code by Damien Elmes +License | GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +Source in | https://github.com/Arthur-Milchior/anki-correct-card-generation +Addon number| [1713990897](https://ankiweb.net/shared/info/1713990897) diff --git a/addons/1713990897/__init__.py b/addons/1713990897/__init__.py new file mode 100644 index 00000000000..ce1f9a5ad35 --- /dev/null +++ b/addons/1713990897/__init__.py @@ -0,0 +1 @@ +from . import init diff --git a/addons/1713990897/ankiweb b/addons/1713990897/ankiweb new file mode 100644 index 00000000000..f1e04ca29ea --- /dev/null +++ b/addons/1713990897/ankiweb @@ -0,0 +1,3 @@ +Ensure that cards are generated if and only if at least one non-empty field is shown. + +For more information, please go to https://github.com/Arthur-Milchior/anki-correct-card-generation diff --git a/addons/1713990897/debug.py b/addons/1713990897/debug.py new file mode 100644 index 00000000000..14abc0ead7f --- /dev/null +++ b/addons/1713990897/debug.py @@ -0,0 +1,49 @@ +from inspect import stack +import re +mayDebug = False + +indentation = 0 +def debug(text, indentToAdd=0, level =1): + global indentation + glob = stack()[level].frame.f_globals + loc = stack()[level].frame.f_locals + text = eval(f"""f"{text}" """,glob,loc) + indentToPrint = indentation + t = " "*indentToPrint + if indentToAdd>0: + t+= "{<" + space = " " + newline = "\n" + t+= re.sub(newline,newline+space,text) + print (t) + indentation +=indentToAdd + if indentToAdd<0: + indentToPrint +=indentToAdd + print((" "*indentToPrint)+">}") + +def debugFun(fun, debug=debug): + if not mayDebug: + return fun + def aux_debugFun(*args, **kwargs): + nonlocal debug + t = f"{fun.__qualname__}(" + first = False + def comma(text): + nonlocal first, t + if not first: + first = True + else: + t+=", " + t+=text + for arg in args: + comma(f"«{arg}»") + for kw in kwargs: + comma(f"«{kw}={kwargs[kw]}»") + t+=")" + debug("{t}",1) + ret = fun(*args, **kwargs) + debug("returns {ret}",-1) + return ret + aux_debugFun.__name__ = f"debug_{fun.__name__}" + aux_debugFun.__qualname__ = f"debug_{fun.__qualname__}" + return aux_debugFun diff --git a/addons/1713990897/init.py b/addons/1713990897/init.py new file mode 100644 index 00000000000..8a17e523fa8 --- /dev/null +++ b/addons/1713990897/init.py @@ -0,0 +1,222 @@ +from anki.template.template import Template, modifiers, modifier, get_or_attr +from anki.collection import _Collection +from anki.cards import Card +from anki.models import ModelManager +from anki.utils import splitFields +import anki +from anki.consts import * +import re +from anki.hooks import runFilter +from anki.sound import stripSounds +from .debug import debugFun + +@modifier(None) +@debugFun +def render_unescaped(self, tag_name=None, context=None): + """Render a tag without escaping it.""" + txt = get_or_attr(context, tag_name) + if txt is not None: + # some field names could have colons in them + # avoid interpreting these as field modifiers + # better would probably be to put some restrictions on field names + showAField = bool(txt.strip())### MODIFIED + return (txt,showAField)### MODIFIED + # field modifiers + parts = tag_name.split(':') + extra = None + if len(parts) == 1 or parts[0] == '': + return ('{unknown field %s}' % tag_name,False) + else: + mods, tag = parts[:-1], parts[-1] #py3k has *mods, tag = parts + + txt = get_or_attr(context, tag) + + #Since 'text:' and other mods can affect html on which Anki relies to + #process clozes, we need to make sure clozes are always + #treated after all the other mods, regardless of how they're specified + #in the template, so that {{cloze:text: == {{text:cloze: + #For type:, we return directly since no other mod than cloze (or other + #pre-defined mods) can be present and those are treated separately + mods.reverse() + mods.sort(key=lambda s: not s=="type") + for mod in mods: + # built-in modifiers + if mod == 'text': + # strip html + txt = stripHTML(txt) if txt else "" + elif mod == 'type': + # type answer field; convert it to [[type:...]] for the gui code + # to process + return ("[[%s]]" % tag_name,False)### MODIFIED + elif mod.startswith('cq-') or mod.startswith('ca-'): + # cloze deletion + mod, extra = mod.split("-") + txt = self.clozeText(txt, extra, mod[1]) if txt and extra else "" + else: + # hook-based field modifier + mod, extra = re.search("^(.*?)(?:\((.*)\))?$", mod).groups() + txt = runFilter('fmod_' + mod, txt or '', extra or '', context, + tag, tag_name) + if txt is None: + return ('{unknown field %s}' % tag_name, False)### MODIFIED + return (txt, True)### MODIFIED +Template.render_unescaped = render_unescaped + + + +@debugFun +def render_tags(self, template, context): + """Renders all the tags in a template for a context. Normally + {{# and {{^ are removed""" + repCount = 0 + showAField = False + while 1: + if repCount > 100: + print("too many replacements") + break + repCount += 1 + + # search for some {{foo}} + match = self.tag_re.search(template) + if match is None: + break + + # + tag, tag_type, tag_name = match.group(0, 1, 2) + tag_name = tag_name.strip() + try: + func = modifiers[tag_type] + replacement = func(self, tag_name, context) + ########## Start new part + if isinstance(replacement,tuple): + replacement, showAField_ = replacement + if showAField_: + showAField = True + ########## End new part + template = template.replace(tag, replacement) + except (SyntaxError, KeyError): + return "{{invalid template}}" + return template, showAField + +Template.render_tags =render_tags + +@debugFun +def render(self, template=None, context=None, encoding=None): + """Turns a Mustache template into something wonderful.""" + template = template or self.template + context = context or self.context + template = self.render_sections(template, context) + + result, showAField = self.render_tags(template, context)#MODIFIED + if encoding is not None: + result = result.encode(encoding) + return result, showAField#MODIFIED +Template.render = render + +@debugFun +def _renderQA(self, data, qfmt=None, afmt=None): + """Returns hash of id, question, answer. + + Keyword arguments: + data -- [cid, nid, mid, did, ord, tags, flds, cardFlags] (see db + documentation for more information about those values) + flds is a list of fields, not a dict. + This corresponds to the information you can obtain in templates, using {{Tags}}, {{Type}}, etc.. + + qfmt -- question format string (as in template) + afmt -- answer format string (as in template) + + unpack fields and create dict + TODO comment better + + """ + cid, nid, mid, did, ord, tags, flds, cardFlags = data + flist = splitFields(flds)#the list of fields + fields = {} # + #name -> ord for each field, tags + # Type: the name of the model, + # Deck, Subdeck: their name + # Card: the template name + # cn: 1 for n being the ord+1 + # FrontSide : + model = self.models.get(mid) + assert model is not None #new (and fieldMap and items were not variables, but directly used + fieldMap = self.models.fieldMap(model) + items = fieldMap.items() + for (name, (idx, conf)) in list(items):#conf is not used + fields[name] = flist[idx] + fields['Tags'] = tags.strip() + fields['Type'] = model['name'] + fields['Deck'] = self.decks.name(did) + fields['Subdeck'] = fields['Deck'].split('::')[-1] + fields['CardFlag'] = self._flagNameFromCardFlags(cardFlags) + if model['type'] == MODEL_STD:#Note that model['type'] has not the same meaning as fields['Type'] + template = model['tmpls'][ord] + else:#for cloze deletions + template = model['tmpls'][0] + fields['Card'] = template['name'] + fields['c%d' % (ord+1)] = "1" + # render q & a + d = dict(id=cid) + # id: card id + qfmt = qfmt or template['qfmt'] + afmt = afmt or template['afmt'] + for (type, format) in (("q", qfmt), ("a", afmt)): + if type == "q":#if/else is in the loop in order for d['q'] to be defined below + format = re.sub("{{(?!type:)(.*?)cloze:", r"{{\1cq-%d:" % (ord+1), format) + #Replace {{'foo'cloze: by {{'foo'cq-(ord+1), where 'foo' does not begins with "type:" + format = format.replace("<%cloze:", "<%%cq:%d:" % ( + ord+1)) + #Replace <%cloze: by <%%cq:(ord+1) + else: + format = re.sub("{{(.*?)cloze:", r"{{\1ca-%d:" % (ord+1), format) + #Replace {{'foo'cloze: by {{'foo'ca-(ord+1) + format = format.replace("<%cloze:", "<%%ca:%d:" % ( + ord+1)) + #Replace <%cloze: by <%%ca:(ord+1) + fields['FrontSide'] = stripSounds(d['q']) + #d['q'] is defined during loop's first iteration + fields = runFilter("mungeFields", fields, model, data, self) # TODO check + html, showAField = anki.template.render(format, fields) #replace everything of the form {{ by its value #MODIFIED + d["showAField"] = showAField#MODIFIED + d[type] = runFilter( + "mungeQA", html, type, fields, model, data, self) # TODO check + # empty cloze? + if type == 'q' and model['type'] == MODEL_CLOZE: + if not self.models._availClozeOrds(model, flds, False): + d['q'] += ("

" + _( + "Please edit this note and add some cloze deletions. (%s)") % ( + "%s" % (HELP_SITE, _("help")))) + #in the case where there is a cloze note type + #without {{cn in fields indicated by + #{{cloze:fieldName; an error message should be + #shown + return d +_Collection._renderQA = _renderQA + +#TOTALLY NEW METHOD +@debugFun +def isEmpty(self): + return not self._getQA()["showAField"] +Card.isEmpty = isEmpty + +#TOTALLY NEW METHOD +@debugFun +def availOrds(self, m, flds): + """ + self -- model manager + m -- a model object + """ + available = [] + flist = splitFields(flds) + fields = {} # + for (name, (idx, conf)) in list(self.fieldMap(m).items()):#conf is not used + fields[name] = flist[idx] + for ord in range(len(m["tmpls"])): + template = m["tmpls"][ord] + format = template['qfmt'] + html, showAField = anki.template.render(format, fields) #replace everything of the form {{ by its value TODO check + if showAField: + available.append(ord) + return available +ModelManager.availOrds = availOrds diff --git a/addons/1713990897/meta.json b/addons/1713990897/meta.json new file mode 100644 index 00000000000..9fd744a9f8f --- /dev/null +++ b/addons/1713990897/meta.json @@ -0,0 +1 @@ +{"name": "More consistent cards generation", "mod": 1562332573} \ No newline at end of file diff --git a/addons/1713990897/zipping b/addons/1713990897/zipping new file mode 100755 index 00000000000..0cf7e4f314e --- /dev/null +++ b/addons/1713990897/zipping @@ -0,0 +1,2 @@ +rm reallyGenerateNonEmpt.zip +zip reallyGenerateNonEmpt.zip *py README.md LICENSE config* zipping ankiweb \ No newline at end of file diff --git a/anki/cards.py b/anki/cards.py index 22fe41431f2..c19318b4415 100644 --- a/anki/cards.py +++ b/anki/cards.py @@ -204,6 +204,10 @@ def a(self): """Return the card answer with its css""" return self.css() + self._getQA()['a'] + def isEmpty(self): + """Whether the card question is empty (i.e. show no field)""" + return not self._getQA()["showAField"] + def css(self): """Return the css of the card's model, as html code""" return "" % self.model()['css'] diff --git a/anki/collection.py b/anki/collection.py index cc0f7b94c49..b5c2496ad18 100644 --- a/anki/collection.py +++ b/anki/collection.py @@ -713,7 +713,7 @@ def renderQA(self, ids=None, type="card"): for row in self._qaData(where)] def _renderQA(self, data, qfmt=None, afmt=None): - """Returns hash of id, question, answer. + """Returns dict with id, question, answer, and whether a field is shown in question. Keyword arguments: data -- [cid, nid, mid, did, ord, tags, flds] (see db @@ -770,7 +770,8 @@ def _renderQA(self, data, qfmt=None, afmt=None): fields['FrontSide'] = stripSounds(d['q']) #d['q'] is defined during loop's first iteration fields = runFilter("mungeFields", fields, model, data, self) # TODO check - html = anki.template.render(format, fields) #replace everything of the form {{ by its value TODO check + html, showAField = anki.template.renderAndIsFieldPresent(format, fields) #replace everything of the form {{ by its value TODO check + d["showAField"] = showAField#MODIFIED d[type] = runFilter( "mungeQA", html, type, fields, model, data, self) # TODO check # empty cloze? diff --git a/anki/models.py b/anki/models.py index a20cc66ecbe..3a647cfa803 100644 --- a/anki/models.py +++ b/anki/models.py @@ -349,6 +349,8 @@ def fieldMap(self, m): """ return dict((f['name'], (f['ord'], f)) for f in m['flds']) + + def fieldNames(self, m): """The list of names of fields of this model.""" return [f['name'] for f in m['flds']] @@ -770,6 +772,36 @@ def availOrds(self, m, flds): should be generated. See ../documentation/templates_generation_rules.md for the detail + """ + if self.col.conf.get("complexTemplates", False): + return self.availOrdsReal(m, flds) + else: + return self.availOrdsOriginal(m, flds) + + + def availOrdsReal(self, m, flds): + """ + self -- model manager + m -- a model object + """ + available = [] + flist = splitFields(flds) + fields = {} # + for (name, (idx, conf)) in list(self.fieldMap(m).items()):#conf is not used + fields[name] = flist[idx] + for ord in range(len(m["tmpls"])): + template = m["tmpls"][ord] + format = template['qfmt'] + html, showAField = anki.template.renderAndIsFieldPresent(format, fields) #replace everything of the form {{ by its value TODO check + if showAField: + available.append(ord) + return available + + def availOrdsOriginal(self, m, flds): + """Given a joined field string, return ordinal of card type which + should be generated. See + ../documentation/templates_generation_rules.md for the detail + """ if m['type'] == MODEL_CLOZE: return self._availClozeOrds(m, flds) diff --git a/anki/template/__init__.py b/anki/template/__init__.py index 7d7306fadf2..fc09b6b8cd2 100644 --- a/anki/template/__init__.py +++ b/anki/template/__init__.py @@ -1,7 +1,7 @@ from anki.template.template import Template from anki.template.view import View -def render(template, context=None, **kwargs): +def renderAndIsFieldPresent(template, context=None, **kwargs): """ Given the template and its fields, create the html of the card. @@ -14,3 +14,6 @@ def render(template, context=None, **kwargs): context = context and context.copy() or {} context.update(kwargs) return Template(template, context).render() + +def render(template, context=None, **kwargs): + return renderAndIsFieldPresent(template, context=context, **kwargs)[0] diff --git a/anki/template/template.py b/anki/template/template.py index ea83e3be361..7dce6bbbfdf 100644 --- a/anki/template/template.py +++ b/anki/template/template.py @@ -77,11 +77,26 @@ def render(self, template=None, context=None, encoding=None): template = template or self.template context = context or self.context + self.showAField = False template = self.render_sections(template, context) result = self.render_tags(template, context) if encoding is not None: result = result.encode(encoding) - return result + return result, self.showAField + + def renderAndIsFieldPresent(self, template=None, context=None, encoding=None): + """A pair with: + * Turns a Mustache template into something wonderful. + * whether a field was shown""" + template = template or self.template + context = context or self.context + + template = self.render_sections(template, context) + self.showAField = False + result = self.render_tags(template, context) + if encoding is not None: + result = result.encode(encoding) + return result, showAField def compile_regexps(self): """Compiles our section and tag regular expressions.""" @@ -134,8 +149,10 @@ def render_sections(self, template, context): return template def render_tags(self, template, context): - """Renders all the tags in a template for a context. Normally - {{# and {{^ are already removed.""" + """A pair with: + * All the tags in a template for a context. Normally + {{# and {{^ are removed, + * whether a field is shown""" repCount = 0 while 1: if repCount > 100: @@ -178,7 +195,9 @@ def render_unescaped(self, tag_name=None, context=None): # some field names could have colons in them # avoid interpreting these as field modifiers # better would probably be to put some restrictions on field names - return txt + if bool(txt.strip()):### MODIFIED + self.showAField = True + return txt### MODIFIED # field modifiers parts = tag_name.split(':') @@ -207,7 +226,7 @@ def render_unescaped(self, tag_name=None, context=None): elif mod == 'type': # type answer field; convert it to [[type:...]] for the gui code # to process - return "[[%s]]" % tag_name + return "[[%s]]" % tag_name, False elif mod.startswith('cq-') or mod.startswith('ca-'): # cloze deletion mod, extra = mod.split("-") @@ -218,8 +237,8 @@ def render_unescaped(self, tag_name=None, context=None): txt = runFilter('fmod_' + mod, txt or '', extra or '', context, tag, tag_name) if txt is None: - return '{unknown field %s}' % tag_name - return txt + return '{unknown field %s}' % tag_name, False + return txt, True def clozeText(self, txt, ord, type): reg = clozeReg diff --git a/aqt/addons.py b/aqt/addons.py index 70275bee3db..5456e056ed8 100644 --- a/aqt/addons.py +++ b/aqt/addons.py @@ -901,6 +901,7 @@ def __hash__(self): """ Set of characteristic of Add-ons incorporated here""" incorporatedAddonsSet = { + Addon("More consistent cards generation", 1713990897, 1562332573, "09567afa0fdfc03474e4c52298d2529556c2ffad", "https://github.com/Arthur-Milchior/anki-correct-card-generation"), } incorporatedAddonsDict = {**{addon.name: addon for addon in incorporatedAddonsSet if addon.name}, diff --git a/aqt/preferences.py b/aqt/preferences.py index 32901cce08c..e8bc698bcf9 100644 --- a/aqt/preferences.py +++ b/aqt/preferences.py @@ -272,6 +272,7 @@ def updateOneOption(self, name, default=False, check=True, sync=True): storeValue[name] = value extraOptions = [ + ("complexTemplates",), ] def setupExtra(self): diff --git a/designer/preferences.ui b/designer/preferences.ui index 0009a7bf405..f582aa80fd2 100644 --- a/designer/preferences.ui +++ b/designer/preferences.ui @@ -7,7 +7,7 @@ 0 0 423 - 508 + 552 @@ -20,7 +20,7 @@ Qt::StrongFocus - 0 + 3 @@ -474,7 +474,7 @@ 0 0 - 401 + 484 471 @@ -489,6 +489,30 @@ + + + + Cards are generated if and only if they show a field. + + + + + + + <html><head/><body><p>This option allow you to create complex card type. Cards risks not to be seen on smartphone, and to be erased by «Empty cards». </p></body></html> + + + true + + + + + + + + + + diff --git a/difference.md b/difference.md index eeaf6f0f9f3..7eebf8cbebb 100644 --- a/difference.md +++ b/difference.md @@ -2,3 +2,9 @@ This files list the difference between regular anki and this forked version. It also lists the different options in the Preferences's extra page. +## Allowing complex card template (1713990897) +All version of anki bugs if you have a complex card template. For example, +if your template is enclosed in {{^field}} and {{/field}}. This option +allow complex card template. The only problem is that official anki +and smartphone apps won't be able to display those cards, and may want +to remove them if you press «check cards». diff --git a/tests/test_models.py b/tests/test_models.py index 39ea748b4f2..a0a19d1e93b 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -112,7 +112,7 @@ def test_cloze_ordinals(): d = getEmptyCol() d.models.setCurrent(d.models.byName("Cloze")) m = d.models.current(); mm = d.models - + #We replace the default Cloze template t = mm.newTemplate("ChainedCloze") t['qfmt'] = "{{text:cloze:Text}}" @@ -120,7 +120,7 @@ def test_cloze_ordinals(): mm.addTemplate(m, t) mm.save(m) d.models.remTemplate(m, m['tmpls'][0]) - + f = d.newNote() f['Text'] = '{{c1::firstQ::firstA}}{{c2::secondQ::secondA}}' d.addNote(f) @@ -129,7 +129,7 @@ def test_cloze_ordinals(): # first card should have first ord assert c.ord == 0 assert c2.ord == 1 - + def test_text(): d = getEmptyCol() @@ -204,7 +204,7 @@ def test_chained_mods(): d = getEmptyCol() d.models.setCurrent(d.models.byName("Cloze")) m = d.models.current(); mm = d.models - + #We replace the default Cloze template t = mm.newTemplate("ChainedCloze") t['qfmt'] = "{{cloze:text:Text}}" @@ -212,7 +212,7 @@ def test_chained_mods(): mm.addTemplate(m, t) mm.save(m) d.models.remTemplate(m, m['tmpls'][0]) - + f = d.newNote() q1 = 'phrase' a1 = 'sentence' From 3834b4a077915c96ff467488f67634447f460f0a Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Thu, 11 Jul 2019 03:52:21 +0200 Subject: [PATCH 52/68] Ensuring showAFied is used consistently --- anki/template/__init__.py | 2 +- anki/template/template.py | 25 ++++++++++--------------- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/anki/template/__init__.py b/anki/template/__init__.py index fc09b6b8cd2..78a4128e4f3 100644 --- a/anki/template/__init__.py +++ b/anki/template/__init__.py @@ -13,7 +13,7 @@ def renderAndIsFieldPresent(template, context=None, **kwargs): """ context = context and context.copy() or {} context.update(kwargs) - return Template(template, context).render() + return Template(template, context).renderAndIsFieldPresent() def render(template, context=None, **kwargs): return renderAndIsFieldPresent(template, context=context, **kwargs)[0] diff --git a/anki/template/template.py b/anki/template/template.py index 7dce6bbbfdf..21545be7e18 100644 --- a/anki/template/template.py +++ b/anki/template/template.py @@ -72,17 +72,9 @@ def __init__(self, template, context=None): self.context = context or {} self.compile_regexps() - def render(self, template=None, context=None, encoding=None): + def render(self, *args, **kwargs): """Turns a Mustache template into something wonderful.""" - template = template or self.template - context = context or self.context - - self.showAField = False - template = self.render_sections(template, context) - result = self.render_tags(template, context) - if encoding is not None: - result = result.encode(encoding) - return result, self.showAField + return self.renderAndIsFieldPresent(*args, **kwargs)[0] def renderAndIsFieldPresent(self, template=None, context=None, encoding=None): """A pair with: @@ -96,7 +88,7 @@ def renderAndIsFieldPresent(self, template=None, context=None, encoding=None): result = self.render_tags(template, context) if encoding is not None: result = result.encode(encoding) - return result, showAField + return result, self.showAField def compile_regexps(self): """Compiles our section and tag regular expressions.""" @@ -208,6 +200,10 @@ def render_unescaped(self, tag_name=None, context=None): mods, tag = parts[:-1], parts[-1] #py3k has *mods, tag = parts txt = get_or_attr(context, tag) + if txt is None: + return '{unknown field %s}' % tag_name + elif bool(txt.strip()):### MODIFIED + self.showAField = True #Since 'text:' and other mods can affect html on which Anki relies to #process clozes, we need to make sure clozes are always @@ -226,7 +222,7 @@ def render_unescaped(self, tag_name=None, context=None): elif mod == 'type': # type answer field; convert it to [[type:...]] for the gui code # to process - return "[[%s]]" % tag_name, False + return "[[%s]]" % tag_name elif mod.startswith('cq-') or mod.startswith('ca-'): # cloze deletion mod, extra = mod.split("-") @@ -236,9 +232,8 @@ def render_unescaped(self, tag_name=None, context=None): mod, extra = re.search(r"^(.*?)(?:\((.*)\))?$", mod).groups() txt = runFilter('fmod_' + mod, txt or '', extra or '', context, tag, tag_name) - if txt is None: - return '{unknown field %s}' % tag_name, False - return txt, True + + return txt def clozeText(self, txt, ord, type): reg = clozeReg From f4058cd543da3a216586ae9fa5f284e81a0436e8 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Sat, 6 Jul 2019 01:51:50 +0200 Subject: [PATCH 53/68] Long term back-up --- addons/529955533/LICENSE | 674 +++++++++++++++++++++++++++++++++++ addons/529955533/README.md | 40 +++ addons/529955533/__init__.py | 52 +++ addons/529955533/ankiweb | 13 + aqt/addons.py | 1 + aqt/main.py | 76 +++- aqt/preferences.py | 1 + designer/preferences.ui | 22 +- difference.md | 6 + 9 files changed, 873 insertions(+), 12 deletions(-) create mode 100644 addons/529955533/LICENSE create mode 100644 addons/529955533/README.md create mode 100644 addons/529955533/__init__.py create mode 100644 addons/529955533/ankiweb diff --git a/addons/529955533/LICENSE b/addons/529955533/LICENSE new file mode 100644 index 00000000000..f288702d2fa --- /dev/null +++ b/addons/529955533/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/addons/529955533/README.md b/addons/529955533/README.md new file mode 100644 index 00000000000..e0f98333fa0 --- /dev/null +++ b/addons/529955533/README.md @@ -0,0 +1,40 @@ +# Regular backups +## Rationale +Sometime, I want to find an old version of my collection. For example, +to see how a card type used to be. Sadly, my oldest back-up is usually +two monthes old. Of course, this can be solved with a [differential backup](https://en.wikipedia.org/wiki/Differential_backup) system; but that is not the point here. + +This add-on ensures that we keep at least one back-up by day for the last month, one back-up by month for the past year, and one back-up by year. + +## Warning +### It can't restore lost backups. +If you install this add-on today, you can be kind of sure that you'll have back-ups of your collection as it is this month/year for the future. However, it should be noted that this add-on can not help you to access your collection as it was before you installed the add-on. Indeed, this add-on can not create data which were already deleted. It only ensures that some data are kept. + +### Missing image +Anki's backup are far from perfect. For example, they do not contains image. Thus you'll see risk to lose images with this add-ons. Actually, this risk already exists if your images are not synced, and that you delete a note with an image, check media, and then restore a backup. + +### New computer +Back-ups are not synchronized. Which means that if you move to another computer, and don't copy the back-up folder, then this add-on become useless. Indeed, we won't have the back-up files. + +### Consider whether you really want to conserve data +As with any backup system, you may think about whether you really want to keep data. If, by accident, you added in anki some private data, and then that you remove it from anki, then those data may still be in the back-up. They will remains in there as long as the back-up exists. Without this add-on, you could be sure that the data would eventually be deleted. With this add-on, if the data are in a yearly backup, then they will never be removed. + +## Internal +This add-on change the method `aqt.main.AnkiQt.backup`, and calls the previous method. + +## Version 2.0 +None + +## TODO +Allow to give more flexibility about how much to save + +## Links, licence and credits + +Key |Value +------------|------------------------------------------------------------------- +Copyright | Arthur Milchior +Based on | Anki code by Damien Elmes +License | GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +Source in | https://github.com/Arthur-Milchior/anki-old-backup +Addon number| [529955533](https://ankiweb.net/shared/info/529955533) +Support me on| [![Ko-fi](https://ko-fi.com/img/Kofi_Logo_Blue.svg)](Ko-fi.com/arthurmilchior) or [![Patreon](http://www.milchior.fr/patreon.png)](https://www.patreon.com/bePatron?u=146206) diff --git a/addons/529955533/__init__.py b/addons/529955533/__init__.py new file mode 100644 index 00000000000..e55b83cc7bb --- /dev/null +++ b/addons/529955533/__init__.py @@ -0,0 +1,52 @@ +import time +import os +from aqt.main import AnkiQt + +oldBackup = AnkiQt.backup +nbDayInMonth = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + +def backup(self, *args, **kwargs): + r = oldBackup(self, *args, **kwargs) + currentTime = time.localtime(time.time()) + year = int(time.strftime("%Y",currentTime)) + month = int(time.strftime("%m",currentTime)) + day = int(time.strftime("%d",currentTime)) + + monthsToKeep = [] + for nbMonth in range(12): + if nbMonth + + +To report bug, and for more informations, please go to https://github.com/Arthur-Milchior/anki-old-backup + +SUPPORT THIS ADD-ON + +Think about giving a thumbs up and about sharing it. + +If you please: All of my contributions are freely shared with every users. They took a lot of time to create and to maintain. If you think that they improve your use of anki, I'd gladly accept a tip: + . \ No newline at end of file diff --git a/aqt/addons.py b/aqt/addons.py index 70275bee3db..360ca31bc1c 100644 --- a/aqt/addons.py +++ b/aqt/addons.py @@ -901,6 +901,7 @@ def __hash__(self): """ Set of characteristic of Add-ons incorporated here""" incorporatedAddonsSet = { + Addon("Long term backups", 529955533, gitHash="cf34f99c6c74e11b49928adb6876123fd1fa83dd", gitRepo = "https://github.com/Arthur-Milchior/anki-old-backup"), } incorporatedAddonsDict = {**{addon.name: addon for addon in incorporatedAddonsSet if addon.name}, diff --git a/aqt/main.py b/aqt/main.py index 4525ba78078..800814283e9 100644 --- a/aqt/main.py +++ b/aqt/main.py @@ -450,28 +450,48 @@ def run(self): z.close() def backup(self): - nbacks = self.pm.profile['numBackups'] - if not nbacks or devMode: + if devMode: return + currentTime = time.localtime(time.time()) + self.year = int(time.strftime("%Y",currentTime)) + self.month = int(time.strftime("%m",currentTime)) + self.day = int(time.strftime("%d",currentTime)) + self.createBackups() + self.cleanRecentBackup() + self.cleanLongTermBackup() + + def createBackups(self): dir = self.pm.backupFolder() path = self.pm.collectionPath() - # do backup - fname = time.strftime("backup-%Y-%m-%d-%H.%M.%S.colpkg", time.localtime(time.time())) - newpath = os.path.join(dir, fname) + localTime = time.localtime(time.time()) + patternsToCreate = {"backup-%Y-%m-%d-%H.%M.%S.colpkg"} + if self.pm.profile.get('longTermBackup', True): + patternsToCreate.update({f"backup-yearly-{self.year:02d}.colpkg", + f"backup-monthly-{self.year:02d}-{self.month:02d}.colpkg", + f"backup-daily-{self.year:02d}-{self.month:02d}-{self.day:02d}.colpkg",}) + filesToCreate = {time.strftime(pattern, localTime) for pattern in patternsToCreate} with open(path, "rb") as f: data = f.read() - b = self.BackupThread(newpath, data) - b.start() + for fname in filesToCreate: + newpath = os.path.join(dir, fname) + if not os.path.exists(newpath): + print(f"Making backup to {newpath}") + b = self.BackupThread(newpath, data) + b.start() + + def cleanRecentBackup(self): + nbacks = self.pm.profile['numBackups'] + if not nbacks: + return + dir = self.pm.backupFolder() # find existing backups backups = [] for file in os.listdir(dir): # only look for new-style format - m = re.match(r"backup-\d{4}-\d{2}-.+.colpkg", file) - if not m: - continue - backups.append(file) + if re.match(r"backup-\d{4}-\d{2}-.+.colpkg", file): + backups.append(file) backups.sort() # remove old ones @@ -480,6 +500,40 @@ def backup(self): path = os.path.join(dir, fname) os.unlink(path) + def cleanLongTermBackup(self): + dir = self.pm.backupFolder() + + nbDayInMonth = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + monthsToKeep = [] + for nbMonth in range(12): + if nbMonth0 0 423 - 508 + 552 @@ -423,6 +423,16 @@ + + + + Also keep some long term back-ups + + + true + + + @@ -489,6 +499,16 @@ + + + + Do some long term backups (days, month, years) + + + true + + + diff --git a/difference.md b/difference.md index eeaf6f0f9f3..f821ce72811 100644 --- a/difference.md +++ b/difference.md @@ -2,3 +2,9 @@ This files list the difference between regular anki and this forked version. It also lists the different options in the Preferences's extra page. +## Long term back-up (529955533) +Ensure that there are both a lot of recent back-up, and one backup by +day for the last month, one by month for the last year, and one by +year. So that you can recover some old part of your collection. + +This can be deactivated from the back-up tab of the preferences window. From 5189b8cfff358fb5a53b2f30f41592e13edfeb1b Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Tue, 9 Jul 2019 15:44:02 +0200 Subject: [PATCH 54/68] limit card numbers using all cards --- anki/bothSched.py | 61 ++++++++++++++++++------------ anki/consts.py | 1 + anki/decks.py | 4 +- anki/sched.py | 84 +++++++++++++++++++++++++++++------------ anki/schedv2.py | 72 ++++++++++++++++++++++++----------- anki/sync.py | 4 +- aqt/addons.py | 1 + aqt/deckconf.py | 13 +++++++ aqt/overview.py | 32 +++++++++------- aqt/preferences.py | 1 + aqt/reviewer.py | 13 ++++--- designer/dconf.ui | 14 +++---- designer/preferences.ui | 10 +++++ difference.md | 10 +++++ 14 files changed, 218 insertions(+), 102 deletions(-) diff --git a/anki/bothSched.py b/anki/bothSched.py index a5900becfd1..de950c68930 100644 --- a/anki/bothSched.py +++ b/anki/bothSched.py @@ -65,16 +65,18 @@ def getCard(self): card.startTimer() return card - def reset(self): + def reset(self, sync=False): """ Deal with the fact that it's potentially a new day. Reset number of learning, review, new cards according to current decks empty queues. Set haveQueues to true + + sync -- whether we need to compute as in original anki, for synchronization to succeed. """ self._updateCutoff() self._resetLrn() - self._resetRev() - self._resetNew() + self._resetRev(sync=sync) + self._resetNew(sync=sync) self._haveQueues = True def dueForecast(self, days=7): @@ -107,11 +109,11 @@ def _updateStats(self, card, type, cnt=1): to this decks and all of its ancestors. """ key = type+"Today" - for g in ([self.col.decks.get(card.did)] + + for deck in ([self.col.decks.get(card.did)] + self.col.decks.parents(card.did)): # add - g[key][1] += cnt - self.col.decks.save(g) + deck[key][1] += cnt + self.col.decks.save(deck) def extendLimits(self, new, rev): """Decrease the limit of new/rev card to see today to this deck, its @@ -123,16 +125,16 @@ def extendLimits(self, new, rev): parents = self.col.decks.parents(cur['id']) children = [self.col.decks.get(did) for (name, did) in self.col.decks.children(cur['id'])] - for g in [cur] + parents + children: + for deck in [cur] + parents + children: # add - g['newToday'][1] -= new - g['revToday'][1] -= rev - self.col.decks.save(g) + deck['newToday'][1] -= new + deck['revToday'][1] -= rev + self.col.decks.save(deck) def _walkingCount(self, limFn=None, cntFn=None): """The sum of cntFn applied to each active deck. - limFn -- function which associate to each deck obejct the maximum number of card to consider + limFn -- function which associate to each deck object the maximum number of card to consider cntFn -- function which, given a deck id and a limit, return a number of card at most equal to this limit.""" tot = 0 pcounts = {}# Associate from each id of a parent deck p, the maximal number of cards of deck p which can be seen, minus the card found for its descendant already considered @@ -176,8 +178,8 @@ def _groupChildren(self, grps): grps -- [deckname, did, rev, lrn, new] """ # first, split the group names into components - for g in grps: - g[0] = g[0].split("::") + for deck in grps: + deck[0] = deck[0].split("::") # and sort based on those components grps.sort(key=itemgetter(0)) # then run main function @@ -186,19 +188,25 @@ def _groupChildren(self, grps): # New cards ########################################################################## - def _resetNewCount(self): - """Set newCount to the counter of new cards for the active decks.""" + def _resetNewCount(self, sync=False): + """ + Set newCount to the counter of new cards for the active decks. + sync -- whether it's called from sync, and the return must satisfies sync sanity check + """ # Number of card in deck did, at most lim def cntFn(did, lim): ret = self.col.db.scalar(f""" select count() from (select 1 from cards where did = ? and queue = {QUEUE_NEW_CRAM} limit ?)""", did, lim) return ret - self.newCount = self._walkingCount(self._deckNewLimitSingle, cntFn) + self.newCount = self._walkingCount(lambda deck:self._deckNewLimitSingle(deck, sync=sync), cntFn) - def _resetNew(self): - """Set newCount, newDids, newCardModulus. Empty newQueue. """ - self._resetNewCount() + def _resetNew(self, sync=False): + """ + Set newCount, newDids, newCardModulus. Empty newQueue. + sync -- whether it's called from sync, and the return must satisfies sync sanity check + """ + self._resetNewCount(sync=sync) self._newDids = self.col.decks.active()[:] self._newQueue = [] self._updateNewCardRatio() @@ -266,8 +274,8 @@ def _deckNewLimit(self, did, fn=None): sel = self.col.decks.get(did) lim = -1 # for the deck and each of its parents - for g in [sel] + self.col.decks.parents(did): - rem = fn(g) + for deck in [sel] + self.col.decks.parents(did): + rem = fn(deck) if lim == -1: lim = rem else: @@ -419,9 +427,12 @@ def log(): # Reviews ########################################################################## - def _resetRev(self): - """Set revCount, empty _revQueue, _revDids""" - self._resetRevCount() + def _resetRev(self, sync=False): + """ + Set revCount, empty _revQueue, _revDids + sync -- whether it's called from sync, and the return must satisfies sync sanity check + """ + self._resetRevCount(sync=sync) self._revQueue = [] def _getRevCard(self): @@ -431,7 +442,7 @@ def _getRevCard(self): def totalRevForCurrentDeck(self): return self.col.db.scalar( - """ + f""" select count() from cards where id in ( select id from cards where did in %s and queue = {QUEUE_REV} and due <= ? limit ?)""" % ids2str(self.col.decks.active()), self.today, self.reportLimit) diff --git a/anki/consts.py b/anki/consts.py index 3ad297b7f1d..87107936df2 100644 --- a/anki/consts.py +++ b/anki/consts.py @@ -139,3 +139,4 @@ def dynOrderLabels(): colNew = "#000099" colRev = "#007700" colDue = "#007700" +colToday = "red" diff --git a/anki/decks.py b/anki/decks.py index f7b679d39c1..229ea0030fa 100644 --- a/anki/decks.py +++ b/anki/decks.py @@ -19,8 +19,8 @@ desc -- deck description, it is shown when cards are learned or reviewd dyn -- 1 if dynamic (AKA filtered) deck, collapsed -- true when deck is collapsed, -extendNew -- extended new card limit (for custom study). Potentially absent, only used in aqt/customstudy.py. By default 10 -extendRev -- extended review card limit (for custom study), Potentially absent, only used in aqt/customstudy.py. By default 10. +extendNew -- extended new card limit (for custom study). Potentially absent, only used in aqt/customstudy.py, it serves to recall the last extension done and put it as default in custom study. By default 10 +extendRev -- similar to extendNew, but for review cards. name -- name of deck, browserCollapsed -- true when deck collapsed in browser, id -- deck ID (automatically generated long), diff --git a/anki/sched.py b/anki/sched.py index 6436cd447c3..60c4a4bc21f 100644 --- a/anki/sched.py +++ b/anki/sched.py @@ -53,11 +53,12 @@ def answerCard(self, card, ease): card.usn = self.col.usn() card.flushSched() - def counts(self, card=None): + def counts(self, card=None, sync=False): """The three numbers to show in anki deck's list/footer. Number of new cards, learning repetition, review card. If cards, then the tuple takes into account the card. + sync -- whether it's called from sync, and the return must satisfies sync sanity check """ counts = [self.newCount, self.lrnCount, self.revCount] if card: @@ -66,6 +67,14 @@ def counts(self, card=None): counts[1] += card.left // 1000 else: counts[idx] += 1 + cur = self.col.decks.current() + conf = self.col.decks.confForDid(cur['id']) + from aqt import mw + if (not sync) and self.col.conf.get("limitAllCards", False): + today = conf['perDay'] - cur['revToday'][1] - cur['newToday'][1] + counts.append(today) + # counts[0] = max(counts[0], today) + # counts[2] = max(counts[2], today) return tuple(counts) def countIdx(self, card): @@ -240,20 +249,27 @@ def _getCard(self): # New cards ########################################################################## - def _deckNewLimitSingle(self, g): - """Maximum number of new card to see today for deck g, not considering parent limit. + def _deckNewLimitSingle(self, deck, sync=False): + """Maximum number of new card to see today for deck deck, not considering parent limit. - If g is a dynamic deck, then reportLimit. + If deck is a dynamic deck, then reportLimit. Otherwise the number of card to see in this deck option, plus the number of card exceptionnaly added to this deck today. keyword arguments: - g -- a deck dictionnary + deck -- a deck dictionnary + sync -- whether it's called from sync, and the return must satisfies sync sanity check """ - if g['dyn']: + if deck['dyn']: return self.reportLimit - c = self.col.decks.confForDid(g['id']) - ret = max(0, c['new']['perDay'] - g['newToday'][1]) - return ret + c = self.col.decks.confForDid(deck['id']) + nbNewToSee = c['new']['perDay'] - deck['newToday'][1] + from aqt import mw + if (not sync) and mw and mw.pm.profile.get("limitAllCards", False): + nbCardToSee = c.get('perDay', 1000) - deck['revToday'][1] - deck['newToday'][1] + limit = min(nbNewToSee, nbCardToSee) + else: + limit = nbNewToSee + return max(0, limit) # Learning queues ########################################################################## @@ -475,19 +491,31 @@ def _lrnForDeck(self, did): # Reviews ########################################################################## - def _deckRevLimit(self, did): - return self._deckNewLimit(did, self._deckRevLimitSingle) + def _deckRevLimit(self, did, sync=False): + """ + sync -- whether it's called from sync, and the return must satisfies sync sanity check + """ + return self._deckNewLimit(did, lambda deck: self._deckRevLimitSingle(deck, sync=sync)) - def _deckRevLimitSingle(self, d): - """Maximum number of card to review today in deck d. + def _deckRevLimitSingle(self, deck, sync=False): + """Maximum number of card to review today in deck deck. self.reportLimit for dynamic deck. Otherwise the number of review according to deck option, plus the number of review added in custom study today. keyword arguments: - d -- a deck object""" - if d['dyn']: + deck -- a deck object + sync -- whether it's called from sync, and the return must satisfies sync sanity check + """ + if deck['dyn']: return self.reportLimit - c = self.col.decks.confForDid(d['id']) - return max(0, c['rev']['perDay'] - d['revToday'][1]) + c = self.col.decks.confForDid(deck['id']) + nbRevToSee = c['rev']['perDay'] - deck['revToday'][1] + from aqt import mw + if (not sync) and mw and mw.pm.profile.get("limitAllCards", False): + nbCardToSee = c.get('perDay', 1000) - deck['revToday'][1] - deck['newToday'][1] + limit = min(nbRevToSee, nbCardToSee) + else: + limit = nbRevToSee + return max(0, limit) def _revForDeck(self, did, lim): """number of cards to review today for deck did @@ -501,8 +529,11 @@ def _revForDeck(self, did, lim): and due <= ? limit ?)""", did, self.today, lim) - def _resetRevCount(self): - """Set revCount""" + def _resetRevCount(self, sync=False): + """ + Set revCount + sync -- whether it's called from sync, and the return must satisfies sync sanity check + """ def cntFn(did, lim): """Number of review cards to see today for deck with id did. At most equal to lim.""" return self.col.db.scalar(f""" @@ -510,10 +541,13 @@ def cntFn(did, lim): did = ? and queue = {QUEUE_REV} and due <= ? limit %d)""" % (lim), did, self.today) self.revCount = self._walkingCount( - self._deckRevLimitSingle, cntFn) + lambda deck: self._deckRevLimitSingle(deck, sync), cntFn) - def _resetRev(self): - super()._resetRev() + def _resetRev(self, sync=False): + """ + sync -- whether it's called from sync, and the return must satisfies sync sanity check + """ + super()._resetRev(sync=sync) self._revDids = self.col.decks.active()[:] def _fillRev(self): @@ -890,11 +924,11 @@ def _updateCutoff(self): self.col.log(self.today, self.dayCutoff) # update all daily counts, but don't save decks to prevent needless # conflicts. we'll save on card answer instead - def update(g): + def update(deck): for t in "new", "rev", "lrn", "time": key = t+"Today" - if g[key][0] != self.today: - g[key] = [self.today, 0] + if deck[key][0] != self.today: + deck[key] = [self.today, 0] for deck in self.col.decks.all(): update(deck) # unbury if the day has rolled over diff --git a/anki/schedv2.py b/anki/schedv2.py index d6e0bcee5eb..480d82c8ea8 100644 --- a/anki/schedv2.py +++ b/anki/schedv2.py @@ -70,11 +70,17 @@ def _answerCardPreview(self, card, ease): self._restorePreviewCard(card) self._removeFromFiltered(card) - def counts(self, card=None): + def counts(self, card=None, sync=False): + """ + sync -- whether it's called from sync, and the return must satisfies sync sanity check + """ counts = [self.newCount, self.lrnCount, self.revCount] if card: idx = self.countIdx(card) counts[idx] += 1 + from aqt import mw + if (not sync) and self.col.conf.get("limitAllCards", False): + counts.append(counts[0] + counts[2]- deck['revToday'][1] - deck['newToday'][1] - deck['lrnToday'][1]) return tuple(counts) def countIdx(self, card): @@ -214,12 +220,21 @@ def _getCard(self): # New cards ########################################################################## - def _deckNewLimitSingle(self, g): - "Limit for deck without parent limits." - if g['dyn']: + def _deckNewLimitSingle(self, deck, sync=False): + """Limit for deck without parent limits. + sync -- whether it's called from sync, and the return must satisfies sync sanity check + """ + if deck['dyn']: return self.dynReportLimit - c = self.col.decks.confForDid(g['id']) - return max(0, c['new']['perDay'] - g['newToday'][1]) + c = self.col.decks.confForDid(deck['id']) + nbNewToSee = c['new']['perDay'] - deck['newToday'][1] + from aqt import mw + if (not sync) and mw and mw.pm.profile.get("limitAllCards", False): + nbCardToSee = c.get('perDay', 1000) - deck['revToday'][1] - deck['newToday'][1] + lim = min(nbNewToSee, nbCardToSee) + else: + lim = nbNewToSee + return max(0, lim) # Learning queues ########################################################################## @@ -435,27 +450,37 @@ def _lrnForDeck(self, did): # Reviews ########################################################################## - def _currentRevLimit(self): - d = self.col.decks.get(self.col.decks.selected(), default=False) - return self._deckRevLimitSingle(d) - - def _deckRevLimitSingle(self, d, parentLimit=None): + def _currentRevLimit(self, sync=False): + """ + sync -- whether it's called from sync, and the return must satisfies sync sanity check + """ + deck = self.col.decks.get(self.col.decks.selected(), default=False) + return self._deckRevLimitSingle(deck, sync=sync) + + def _deckRevLimitSingle(self, deck, parentLimit=None, sync=False): + """ + sync -- whether it's called from sync, and the return must satisfies sync sanity check + """ # invalid deck selected? - if not d: + if not deck: return 0 - if d['dyn']: + if deck['dyn']: return self.dynReportLimit - c = self.col.decks.confForDid(d['id']) - lim = max(0, c['rev']['perDay'] - d['revToday'][1]) + c = self.col.decks.confForDid(deck['id']) + lim = max(0, c['rev']['perDay'] - deck['revToday'][1]) + from aqt import mw + if (not sync) and mw and mw.pm.profile.get("limitAllCards", False): + nbCardToSee = c.get('perDay', 1000) - deck['revToday'][1] - deck['newToday'][1] + lim = min(lim, nbCardToSee) if parentLimit is not None: return min(parentLimit, lim) - elif '::' not in d['name']: + elif '::' not in deck['name']: return lim else: - for parent in self.col.decks.parents(d['id']): + for parent in self.col.decks.parents(deck['id']): # pass in dummy parentLimit so we don't do parent lookup again lim = min(lim, self._deckRevLimitSingle(parent, parentLimit=lim)) return lim @@ -470,8 +495,11 @@ def _revForDeck(self, did, lim, childMap): and due <= ? limit ?)""" % ids2str(dids), self.today, lim) - def _resetRevCount(self): - lim = self._currentRevLimit() + def _resetRevCount(self, sync=False): + """ + sync -- whether it's called from sync, and the return must satisfies sync sanity check + """ + lim = self._currentRevLimit(sync=sync) self.revCount = self.col.db.scalar(f""" select count() from (select id from cards where did in %s and queue = {QUEUE_REV} and due <= ? limit {lim})""" % @@ -779,11 +807,11 @@ def _updateCutoff(self): self.col.log(self.today, self.dayCutoff) # update all daily counts, but don't save decks to prevent needless # conflicts. we'll save on card answer instead - def update(g): + def update(deck): for t in "new", "rev", "lrn", "time": key = t+"Today" - if g[key][0] != self.today: - g[key] = [self.today, 0] + if deck[key][0] != self.today: + deck[key] = [self.today, 0] for deck in self.col.decks.all(): update(deck) # unbury if the day has rolled over diff --git a/anki/sync.py b/anki/sync.py index 590afc6c560..3e36a61a9b8 100644 --- a/anki/sync.py +++ b/anki/sync.py @@ -262,12 +262,12 @@ def sanityCheck(self): return "model had usn = -1" if found: self.col.models.save() - self.col.sched.reset() + self.col.sched.reset(sync=True) # check for missing parent decks self.col.sched.deckDueList() # return summary of deck return [ - list(self.col.sched.counts()), + list(self.col.sched.counts(sync=True)), self.col.db.scalar("select count() from cards"), self.col.db.scalar("select count() from notes"), self.col.db.scalar("select count() from revlog"), diff --git a/aqt/addons.py b/aqt/addons.py index 70275bee3db..1d7837f6b02 100644 --- a/aqt/addons.py +++ b/aqt/addons.py @@ -901,6 +901,7 @@ def __hash__(self): """ Set of characteristic of Add-ons incorporated here""" incorporatedAddonsSet = { + Addon("Limit number of cards by day both new and review", 602339056, 1562849512, "72f2ea268fa8b116f9aecde968dc0aa324b33636", "https://github.com/Arthur-Milchior/anki-limit-to-both-new-and-revs"), } incorporatedAddonsDict = {**{addon.name: addon for addon in incorporatedAddonsSet if addon.name}, diff --git a/aqt/deckconf.py b/aqt/deckconf.py index dcad9b97186..b368ac47bd0 100644 --- a/aqt/deckconf.py +++ b/aqt/deckconf.py @@ -5,6 +5,7 @@ from anki.consts import NEW_CARDS_RANDOM from aqt.qt import * +from PyQt5 import QtWidgets import aqt from aqt.utils import showInfo, showWarning, openHelp, getOnlyText, askUser, \ tooltip, saveGeom, restoreGeom @@ -20,6 +21,14 @@ def __init__(self, mw, deck): self._origNewOrder = None self.form = aqt.forms.dconf.Ui_Dialog() self.form.setupUi(self) + if mw.pm.profile.get("limitAllCards", False): + self.form.totalPerDay = QtWidgets.QSpinBox(self.form.tab_5) + self.form.totalPerDay.setObjectName("totalPerDay") + self.form.gridLayout_5.addWidget(self.form.totalPerDay, 1, 1, 1, 1) + self.form.label_16 = QtWidgets.QLabel(self.form.tab_5) + self.form.label_16.setObjectName("label_16") + self.form.gridLayout_5.addWidget(self.form.label_16, 1, 0, 1, 1) + self.form.label_16.setText(_("total card/day")) self.mw.checkpoint(_("Options")) self.setupCombos() self.setupConfs() @@ -198,6 +207,8 @@ def loadConf(self): f.leechAction.setCurrentIndex(c['leechAction']) # general c = self.conf + if self.mw.pm.profile.get("limitAllCards", False): + f.totalPerDay.setValue(c.get('perDay', 1000)) f.maxTaken.setValue(c['maxTaken']) f.showTimer.setChecked(c.get('timer', 0)) f.autoplaySounds.setChecked(c['autoplay']) @@ -285,6 +296,8 @@ def saveConf(self): c['timer'] = f.showTimer.isChecked() and 1 or 0 c['autoplay'] = f.autoplaySounds.isChecked() c['replayq'] = f.replayQuestion.isChecked() + if self.mw.pm.profile.get("limitAllCards", False): + c['perDay'] = f.totalPerDay.value() # description self.deck['desc'] = f.desc.toPlainText() self.mw.col.decks.save(self.deck) diff --git a/aqt/overview.py b/aqt/overview.py index 9c513fac337..eaac6623aae 100644 --- a/aqt/overview.py +++ b/aqt/overview.py @@ -172,20 +172,24 @@ def _table(self): return '

%s
' % ( self.mw.col.sched.finishedMsg()) else: - return f''' - -
- - - - -
%s:%s
%s:%s
%s:%s
-
-%s
''' % ( - _("New"), counts[0], - _("Learning"), counts[1], - _("To Review"), counts[2], - but("study", _("Study Now"), id="study",extra=" autofocus")) + l = [ + (_("Learning"), counts[1], colLearn), + (_("New"), counts[0], colNew), + (_("To Review"), counts[2], colRev), + ] + from aqt import mw + if mw and mw.pm.profile.get("limitAllCards", False): + l.append(("""{_("New")} + {_("To Review")}""", counts[3], colToday)) + return (''' + +
+ ''' + +"\n ".join([f'''''' + for string, count, col in l]) + +f'''\ +
{string}:{count}
+
+ {but("study", _("Study Now"), id="study",extra=" autofocus")}
''') _body = """ diff --git a/aqt/preferences.py b/aqt/preferences.py index 32901cce08c..2742f65c700 100644 --- a/aqt/preferences.py +++ b/aqt/preferences.py @@ -272,6 +272,7 @@ def updateOneOption(self, name, default=False, check=True, sync=True): storeValue[name] = value extraOptions = [ + ("limitAllCards", True), ] def setupExtra(self): diff --git a/aqt/reviewer.py b/aqt/reviewer.py index db9ce2129fc..4c822946e11 100644 --- a/aqt/reviewer.py +++ b/aqt/reviewer.py @@ -524,13 +524,16 @@ def _remaining(self): counts = list(self.mw.col.sched.counts()) else: counts = list(self.mw.col.sched.counts(self.card)) + if self.mw.pm.profile.get("limitAllCards", False): + counts[0] = min(counts[0], counts[3]) + counts[2] = min(counts[2], counts[3]) idx = self.mw.col.sched.countIdx(self.card) counts[idx] = "%s" % (counts[idx]) - space = " + " - ctxt = f'%s' % counts[0] - ctxt += space + f'%s' % counts[1] - ctxt += space + f'%s' % counts[2] - return ctxt + return " + ".join([f'{count}' + for col, count in [ + (colNew, counts[0]), + (colLearn, counts[1]), + (colRev, counts[2]),]]) def _defaultEase(self): if self.mw.col.sched.answerButtons(self.card) == 4: diff --git a/designer/dconf.ui b/designer/dconf.ui index 2cb6bc9ab22..e5e90b6de5e 100644 --- a/designer/dconf.ui +++ b/designer/dconf.ui @@ -589,13 +589,6 @@ 12 - - - - Ignore answer times longer than - - - @@ -616,6 +609,13 @@ + + + + Ignore answer times longer than + + + diff --git a/designer/preferences.ui b/designer/preferences.ui index 0009a7bf405..2e059e31056 100644 --- a/designer/preferences.ui +++ b/designer/preferences.ui @@ -399,6 +399,9 @@ 16777215 + + 99999 + @@ -489,6 +492,13 @@ + + + + Limit the total number of cards seen by day in a deck + + + diff --git a/difference.md b/difference.md index eeaf6f0f9f3..7916517ba82 100644 --- a/difference.md +++ b/difference.md @@ -2,3 +2,13 @@ This files list the difference between regular anki and this forked version. It also lists the different options in the Preferences's extra page. +## Limit the total number of cards seen today +In the preferences, you can check "Limit the total number of cards +seen by day in a deck". If you do so, in the deck's option's +configuration window, in the "general" tab, you can decide the maximal +number of cards you see in the deck. This put a limit on the sum of +both reviewed cards and new cards. So, the days where you have a lot +of cards to review, you'll have few or no new cards, and you'll have +more cards if you did have little to review. + +A fourth number will thus be shown in the deck overview windows. From 8912ace5f17ab27ecfc8334d6b78bf1768f87992 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Mon, 17 Jun 2019 15:15:33 +0200 Subject: [PATCH 55/68] Consider name when changing type of a note --- addons/513858554/LICENSE | 674 ++++++++++++++++++++++++++++++ addons/513858554/README.md | 72 ++++ addons/513858554/__init__.py | 6 + addons/513858554/bidirectional.py | 59 +++ addons/513858554/changeCards.py | 31 ++ addons/513858554/cloze.py | 45 ++ addons/513858554/config.json | 1 + addons/513858554/config.py | 30 ++ addons/513858554/getMap.py | 41 ++ addons/513858554/meta.json | 1 + addons/513858554/rebuild.py | 61 +++ addons/513858554/twoLists.py | 38 ++ anki/utils.py | 37 ++ aqt/addons.py | 1 + aqt/browser.py | 103 ++++- aqt/preferences.py | 1 + designer/preferences.ui | 12 +- difference.md | 8 + 18 files changed, 1198 insertions(+), 23 deletions(-) create mode 100644 addons/513858554/LICENSE create mode 100644 addons/513858554/README.md create mode 100644 addons/513858554/__init__.py create mode 100644 addons/513858554/bidirectional.py create mode 100644 addons/513858554/changeCards.py create mode 100644 addons/513858554/cloze.py create mode 100644 addons/513858554/config.json create mode 100644 addons/513858554/config.py create mode 100644 addons/513858554/getMap.py create mode 100644 addons/513858554/meta.json create mode 100644 addons/513858554/rebuild.py create mode 100644 addons/513858554/twoLists.py diff --git a/addons/513858554/LICENSE b/addons/513858554/LICENSE new file mode 100644 index 00000000000..f288702d2fa --- /dev/null +++ b/addons/513858554/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/addons/513858554/README.md b/addons/513858554/README.md new file mode 100644 index 00000000000..7326ba2d2a9 --- /dev/null +++ b/addons/513858554/README.md @@ -0,0 +1,72 @@ +# Improving: Change note type +This add-on improves «Change note type» in two related ways. +## Rationale +### clozed note +Currently, anki has a limitation when you want to change a card type +from or to a clozed note type. + +Without this add-on, if you want to change a standard note to a clozed +note type, you can move a single card, and it will move to cloze 1. + +If you want to change a clozed note type to a standard type, only +cloze 1 will be changed. + +With this add-on, you'll be able to change any card type to any cloze +number and reciprocally. The only restriction is that you need to have +the cloze number in the fields before changing TO a clozed note +type. Because this add-on will only allow you to choose one of the +cloze number appearing in a field. + +### Considering when fields and card types have the same name +When I use anki, I may multiple note types having a card types named +«definition», for example. When I change the note type of a card, I +then want, by default, the card «definition» to be mapped to the card +«definition», even if they are not in the same position in the list of +card type. + +This add-on does that. It ensures that, by default, when source and +target note types have a card types/fields which have the same name, +then by default, they map to each other. + +This can be deactivated in the configuration. + +Default «change note type» window occuring with this add-on. +![Example](example.png) + + +## Warning +This add-on was created 3rd of June 2019. It is still new, and may +have unfound bugs. So please, before using it, make a back-up, check +whether the note type change went allright, and let me know either if +it worked or if you had a bug + +## Configuration +There is a single configuration option. If you set "Associate to same +name" to `true`, the add-ons will set the default option of each card +types/fields to be the card types/fields with the same +name. Otherwise, if it is set to `false`, the add-on will keep the +default way. + +## Internal +It changes methods: +* `aqt.browser.ChangeModel.rebuildTemplateMap` +* `aqt.browser.ChangeModel.rebuildFieldMap` +* `aqt.browser.ChangeModel.getTemplateMap` +* `aqt.browser.ChangeModel.getFieldMap` +* `anki.models.ModelManager._changeCards` + + +## Version 2.0 +None + + +## Links, licence and credits + +Key |Value +------------|------------------------------------------------------------------- +Copyright | Arthur Milchior +Based on | Anki code by Damien Elmes +License | GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +Source in | https://github.com/Arthur-Milchior/anki-change-note-type-clozes.git +Addon number| [513858554](https://ankiweb.net/shared/info/513858554) +Support me on| [![Ko-fi](https://ko-fi.com/img/Kofi_Logo_Blue.svg)](Ko-fi.com/arthurmilchior) or [![Patreon](http://www.milchior.fr/patreon.png)](https://www.patreon.com/bePatron?u=146206) diff --git a/addons/513858554/__init__.py b/addons/513858554/__init__.py new file mode 100644 index 00000000000..0aa39a908b3 --- /dev/null +++ b/addons/513858554/__init__.py @@ -0,0 +1,6 @@ +from . import cloze, getMap, rebuild + + + + + diff --git a/addons/513858554/bidirectional.py b/addons/513858554/bidirectional.py new file mode 100644 index 00000000000..f0c9599137e --- /dev/null +++ b/addons/513858554/bidirectional.py @@ -0,0 +1,59 @@ +class Bidirectional: + def __init__(self, direct = None, inverse = None): + if isinstance(direct, dict) and isinstance(inverse, dict): + self.direct = direct + self.inverse = inverse + return + self.direct = dict() + self.inverse = dict() + if isinstance(direct, list): + for counter, value in enumerate(direct): + self[counter] = value + + + def getByValue(self, value): + return self.exchange()[value] + + def __get__(self, key): + if key in self.direct: + return self.direct(key) + raise AttributeError + + def __set__(self, key, value): + self.direct[key] = value + self.inverse[value] = key + + def __delete__(self, key): + del self.inverse[self.direct[key]] + del self.direct[key] + + def deleteValue(self, value): + del self.changeSide()[value] + + def exchange(self, key1, key2): + value1 = self[key1] + value2 = self[key2] + self[key1] = value2 + self[key2] = value1 + + def exchangeValue(self, value1, value2): + self.changeSide().exchange(value1, value2) + + def changeSide(self): + """A Bidirectionnal with key and value reversed. They have both the + same content. + + """ + return Bidirectionnal(self.inverse, self.direct) + + def exchange(self, key1, key2): + value1 = self[key1] + value2 = self[key2] + self[key1] = value2 + self[key2] = value1 + + def __contains__(self, key): + return key in self.direct + + def isValue(self, value): + return value in self.inverse diff --git a/addons/513858554/changeCards.py b/addons/513858554/changeCards.py new file mode 100644 index 00000000000..7e085007e4c --- /dev/null +++ b/addons/513858554/changeCards.py @@ -0,0 +1,31 @@ +from anki.utils import ids2str, intTime +from anki.models import ModelManager + +#removed the special case of MODEL_CLOZE +def _changeCards(self, nids, oldModel, newModel, map): + """Change the note whose ids are nid to the model newModel, reorder + fields according to map. Write the change in the database + Remove the cards mapped to nothing + If the source is a cloze, it is (currently?) mapped to the + card of same order in newModel, independtly of map. + keyword arguments: + nids -- the list of id of notes to change + oldModel -- the soruce model of the notes + newmodel -- the model of destination of the notes + map -- the dictionnary sending to each card 'ord of the old model a card'ord of the new model or to None + """ + d = [] + deleted = [] + for (cid, ord) in self.col.db.execute( + "select id, ord from cards where nid in "+ids2str(nids)): + new = map[ord] + if new is not None: + d.append(dict( + cid=cid,new=new,u=self.col.usn(),m=intTime())) + else: + deleted.append(cid) + self.col.db.executemany( + "update cards set ord=:new,usn=:u,mod=:m where id=:cid", + d) + self.col.remCards(deleted) +ModelManager._changeCards = _changeCards diff --git a/addons/513858554/cloze.py b/addons/513858554/cloze.py new file mode 100644 index 00000000000..f278e24c6e9 --- /dev/null +++ b/addons/513858554/cloze.py @@ -0,0 +1,45 @@ +from anki.consts import * +from anki.models import ModelManager +from anki.utils import ids2str +from aqt import mw +import re + +def getClozeNumberFromNids(nids): + """Given a list of nids, return the set of cloze number in the fields + of notes with one of those nids. + + """ + s = set() + for flds in mw.col.db.list(f"""select flds from notes where id in {ids2str(nids)} """): + getClozeNumberFromFields(flds, s) + return s + +def getClozeNumber(changeModelWindow): + """The list of cloze number contained in the fields of some notes + whose nid belongs to ChangeModel window. + + """ + return list(getClozeNumberFromNids(changeModelWindow.nids)) + +#from anki.models.ModelManager._availClozeOrds +clozeFinder = re.compile(r"(?s){{c(?P\d+)::.+?}}") +def getClozeNumberFromFields(flds, s): + """Add to the set s the cloze number of flds. + + """ + for match in clozeFinder.finditer(flds): + number = match.group("number") + s.add(int(number)) + return s + + +def getTemplateList(self, model): + """The list of template + + For a normal template, it's actually the list of template. + Otherwise, it is, in increasing order, the list of clozes used in self.nid + """ + if model['type'] == MODEL_STD: + return model["tmpls"] + else: + return [{"name":f"Cloze {n}", "ord":n-1} for n in sorted(getClozeNumber(self))] diff --git a/addons/513858554/config.json b/addons/513858554/config.json new file mode 100644 index 00000000000..09633d4b832 --- /dev/null +++ b/addons/513858554/config.json @@ -0,0 +1 @@ +{"Associate to same name":true} diff --git a/addons/513858554/config.py b/addons/513858554/config.py new file mode 100644 index 00000000000..11cf99612d1 --- /dev/null +++ b/addons/513858554/config.py @@ -0,0 +1,30 @@ +from aqt import mw +import sys + +userOption = None + +def getUserOption(key = None, default = None): + #print(f"getUserOption(key = {key}, default = {default})") + global userOption + if userOption is None: + userOption = mw.addonManager.getConfig(__name__) + #debug("userOption read from the file and is {userOption}") + if key is None: + #debug("return {userOption}") + return userOption + if key in userOption: + #debug("key in userOption. Returning {userOption[key]}") + return userOption[key] + else: + #debug("key not in userOption. Returning default.") + return default + +def writeConfig(): + mw.addonManager.writeConfig(__name__,userOption) + +def update(_): + global userOption, fromName + userOption = None + fromName = None + +mw.addonManager.setConfigUpdatedAction(__name__,update) diff --git a/addons/513858554/getMap.py b/addons/513858554/getMap.py new file mode 100644 index 00000000000..ab314210840 --- /dev/null +++ b/addons/513858554/getMap.py @@ -0,0 +1,41 @@ +from aqt.browser import ChangeModel +from .cloze import getTemplateList + +def _getMap(self, old=None, combos=None, new=None): + """A map from template's ord of the old model to template's ord of the new + model. Or None if no template + Contrary to what this name indicates, the method may be used + without templates. In getFieldMap it is used for fields + keywords parameter: + old -- the list of templates of the old model + combos -- the python list of gui's list of template + new -- the list of templates of the new model + If old is not given, the other two arguments are not used. + """ + map = {} + for i, f in enumerate(old): + idx = combos[i].currentIndex() + if idx == len(new): + # ignore. len(new) corresponds "Nothing" in the list + map[f['ord']] = None + else: + f2 = new[idx] + map[f['ord']] = f2['ord'] + return map + +def getFieldMap(self): + """Associating to each field's ord of the source model a field's + ord (or None) of the new model.""" + return _getMap(self, + self.oldModel['flds'], + self.fcombos, + self.targetModel['flds']) + +def getTemplateMap(self): + return _getMap(self, + getTemplateList(self, self.oldModel), + self.tcombos, + getTemplateList(self, self.targetModel)) + +ChangeModel.getTemplateMap = getTemplateMap +ChangeModel.getFieldMap = getFieldMap diff --git a/addons/513858554/meta.json b/addons/513858554/meta.json new file mode 100644 index 00000000000..aa6e24e806c --- /dev/null +++ b/addons/513858554/meta.json @@ -0,0 +1 @@ +{"name": "Improving change note type", "mod": 1560753393} \ No newline at end of file diff --git a/addons/513858554/rebuild.py b/addons/513858554/rebuild.py new file mode 100644 index 00000000000..9b77a940f5f --- /dev/null +++ b/addons/513858554/rebuild.py @@ -0,0 +1,61 @@ +from anki.lang import _ +from .cloze import getTemplateList +from .twoLists import eltToPos +from aqt.browser import ChangeModel +from .config import getUserOption +from aqt.qt import QGridLayout, QWidget, QComboBox, QLabel + +def rebuildTemplateMap(self): + self.tsrc = getTemplateList(self, self.oldModel) + self.tdst = getTemplateList(self, self.targetModel) + return _rebuildMap(self, "t") + +ChangeModel.rebuildTemplateMap = rebuildTemplateMap + +def rebuildFieldMap(self): + self.fsrc = self.oldModel["flds"] + self.fdst = self.targetModel["flds"] + return _rebuildMap(self, "f") +ChangeModel.rebuildFieldMap = rebuildFieldMap + + +def _rebuildMap(self, key): + """ + key -- either "t" or "f", for template or fields. What to edit. + + """ + map = getattr(self, key + "widg") + lay = getattr(self, key + "layout") + if map: + lay.removeWidget(map) + map.deleteLater() + setattr(self, key + "MapWidget", None) + map = QWidget() + l = QGridLayout() + combos = [] + targets = [template['name'] for template in getattr(self, key+"dst")] + indices = {} + sources = getattr(self,key+"src") + sourcesNames = [template["name"] for template in sources] + if getUserOption("Associate to same name"): + assoc = eltToPos(sourcesNames, targets) + else: + assoc = enumerate(sourcesNames) + for indexSrc, (indexDst, templateName) in enumerate(assoc): + l.addWidget(QLabel(_("Change %s to:") % templateName), indexSrc, 0) + cb = QComboBox() + cb.addItems(targets + [_("Nothing")]) + idx = min(indexDst, len(targets)) + cb.setCurrentIndex(idx) + indices[cb] = idx + cb.currentIndexChanged.connect( + lambda i, cb=cb, key=key: self.onComboChanged(i, cb, key)) + combos.append(cb) + l.addWidget(cb, indexSrc, 1) + map.setLayout(l) + lay.addWidget(map) + setattr(self, key + "widg", map) + setattr(self, key + "layout", lay) + setattr(self, key + "combos", combos) + setattr(self, key + "indices", indices) + diff --git a/addons/513858554/twoLists.py b/addons/513858554/twoLists.py new file mode 100644 index 00000000000..a457a14bb91 --- /dev/null +++ b/addons/513858554/twoLists.py @@ -0,0 +1,38 @@ +def eltToElt(list1, list2): + """A list associating to each element elt of list1, either an element of list 2, or None. + + If elt is in list2, it is associated to itself. Each element of + list1 is associated to a distinct element of list2. If they are + more element in list2 than in list1, then the last elements of + list1 are associated to None. + + """ + set1 = set(list1) + set2 = set(list2) + used2 = set() + mapping = [] + usableValue = [value for value in list2 if value not in set1] + nbUsableValues = len(usableValue) + usedValues = 0 + for key in list1: + if key in set2: + value = key + elif usedValues < nbUsableValues: + value = usableValue[usedValues] + usedValues +=1 + else: + value = None + mapping.append((key,value)) + #print(f"eltToElt({list1},{list2}) = {mapping}") + return mapping + + +def eltToPos(list1, list2): + """Similar to eltToElt, but also associate the position of the value in list2. Or the length of list2 its not a value.""" + reverse = dict() + l = len(list2) + for pos, elt in enumerate(list2): + reverse[elt] = pos + mapping = [(reverse.get(value, l), key) for key, value in eltToElt(list1, list2)] + #print(f"eltToElt({list1},{list2}) = {mapping}") + return mapping diff --git a/anki/utils.py b/anki/utils.py index bc52d186c91..f8b788e6f04 100644 --- a/anki/utils.py +++ b/anki/utils.py @@ -444,3 +444,40 @@ def versionWithBuild(): except: build = "dev" return "%s (%s)" % (version, build) + +def eltToElt(list1, list2): + """A list associating to each element elt of list1, either an element of list 2, or None. + + If elt is in list2, it is associated to itself. Each element of + list1 is associated to a distinct element of list2. If they are + more element in list2 than in list1, then the last elements of + list1 are associated to None. + + """ + set1 = set(list1) + set2 = set(list2) + used2 = set() + mapping = [] + usableValue = [value for value in list2 if value not in set1] + nbUsableValues = len(usableValue) + usedValues = 0 + for key in list1: + if key in set2: + value = key + elif usedValues < nbUsableValues: + value = usableValue[usedValues] + usedValues +=1 + else: + value = None + mapping.append((key,value)) + return mapping + + +def eltToPos(list1, list2): + """Similar to eltToElt, but also associate the position of the value in list2. Or the length of list2 its not a value.""" + reverse = dict() + l = len(list2) + for pos, elt in enumerate(list2): + reverse[elt] = pos + mapping = [(reverse.get(value, l), key) for key, value in eltToElt(list1, list2)] + return mapping diff --git a/aqt/addons.py b/aqt/addons.py index 70275bee3db..a692e1df53f 100644 --- a/aqt/addons.py +++ b/aqt/addons.py @@ -901,6 +901,7 @@ def __hash__(self): """ Set of characteristic of Add-ons incorporated here""" incorporatedAddonsSet = { + Addon("Improving change note type", 513858554, 1560753393, "4ece9f1da85358bce05a75d3bbeffa91d8c17ad4", "https://github.com/Arthur-Milchior/anki-change-note-type-clozes"), } incorporatedAddonsDict = {**{addon.name: addon for addon in incorporatedAddonsSet if addon.name}, diff --git a/aqt/browser.py b/aqt/browser.py index f0e74fef0a0..8a3eeb579ff 100644 --- a/aqt/browser.py +++ b/aqt/browser.py @@ -16,7 +16,7 @@ import aqt.forms from anki.utils import fmtTimeSpan, ids2str, htmlToTextLine, \ isWin, intTime, \ - isMac, bodyClass + isMac, bodyClass, eltToPos from aqt.utils import saveGeom, restoreGeom, saveSplitter, restoreSplitter, \ saveHeader, restoreHeader, saveState, restoreState, getTag, \ showInfo, askUser, tooltip, openHelp, showWarning, shortcut, mungeQA, \ @@ -2015,19 +2015,20 @@ def modelChanged(self, model): self.rebuildTemplateMap() self.rebuildFieldMap() - def rebuildTemplateMap(self, key=None, attr=None): + def rebuildTemplateMap(self): + self.tsrc = self.getTemplateList(self.oldModel) + self.tdst = self.getTemplateList(self.targetModel) + return self._rebuildMap("t") + + def _rebuildMap(self, key=None): """Change the "Cards" subwindow of the Change Note Type. Actually, if key and attr are given, it may change another subwindow, so the same code is reused for fields. + key -- either "t" or "f", for template or fields. What to edit. """ - if not key: - key = "t" - attr = "tmpls" map = getattr(self, key + "widg") lay = getattr(self, key + "layout") - src = self.oldModel[attr] - dst = self.targetModel[attr] if map: lay.removeWidget(map) map.deleteLater() @@ -2035,19 +2036,25 @@ def rebuildTemplateMap(self, key=None, attr=None): map = QWidget() l = QGridLayout() combos = [] - targets = [x['name'] for x in dst] + [_("Nothing")] + targets = [template['name'] for template in getattr(self, key+"dst")] indices = {} - for i, x in enumerate(src): - l.addWidget(QLabel(_("Change %s to:") % x['name']), i, 0) + sources = getattr(self,key+"src") + sourcesNames = [template["name"] for template in sources] + if self.col.conf.get("preserveName", True): + assoc = eltToPos(sourcesNames, targets) + else: + assoc = enumerate(sourcesNames) + for indexSrc, (indexDst, templateName) in enumerate(assoc): + l.addWidget(QLabel(_("Change %s to:") % templateName), indexSrc, 0) cb = QComboBox() - cb.addItems(targets) - idx = min(i, len(targets)-1) + cb.addItems(targets + [_("Nothing")]) + idx = min(indexDst, len(targets)) cb.setCurrentIndex(idx) indices[cb] = idx cb.currentIndexChanged.connect( lambda i, cb=cb, key=key: self.onComboChanged(i, cb, key)) combos.append(cb) - l.addWidget(cb, i, 1) + l.addWidget(cb, indexSrc, 1) map.setLayout(l) lay.addWidget(map) setattr(self, key + "widg", map) @@ -2057,7 +2064,9 @@ def rebuildTemplateMap(self, key=None, attr=None): def rebuildFieldMap(self): """Change the "Fields" subwindow of the Change Note Type.""" - return self.rebuildTemplateMap(key="f", attr="flds") + self.fsrc = self.oldModel["flds"] + self.fdst = self.targetModel["flds"] + return self._rebuildMap("f") def onComboChanged(self, i, cb, key): indices = getattr(self, key + "indices") @@ -2079,6 +2088,44 @@ def onComboChanged(self, i, cb, key): break indices[cb] = i + def getClozeNumberFromFields(self, flds, s): + """Add to the set s the cloze number of flds. + + """ + clozeFinder = re.compile(r"(?s){{c(?P\d+)::.+?}}") + for match in clozeFinder.finditer(flds): + number = match.group("number") + s.add(int(number)) + return s + + def getClozeNumberFromNids(self, nids): + """Given a list of nids, return the set of cloze number in the fields + of notes with one of those nids. + + """ + s = set() + for flds in self.browser.col.db.list(f"""select flds from notes where id in {ids2str(nids)} """): + self.getClozeNumberFromFields(flds, s) + return s + + def getClozeNumber(self): + """The list of cloze number contained in the fields of some notes + whose nid belongs to ChangeModel window. + + """ + return list(self.getClozeNumberFromNids(self.nids)) + + def getTemplateList(self, model): + """The list of template + + For a normal template, it's actually the list of template. + Otherwise, it is, in increasing order, the list of clozes used in self.nid + """ + if model['type'] == MODEL_STD: + return model["tmpls"] + else: + return [{"name":f"Cloze {n}", "ord":n-1} for n in sorted(self.getClozeNumber())] + def getTemplateMap(self, old=None, combos=None, new=None): """A map from template's ord of the old model to template's ord of the new model. Or None if no template @@ -2092,10 +2139,22 @@ def getTemplateMap(self, old=None, combos=None, new=None): new -- the list of templates of the new model If old is not given, the other two arguments are not used. """ - if not old: - old = self.oldModel['tmpls'] - combos = self.tcombos - new = self.targetModel['tmpls'] + return self._getMap( + self.getTemplateList(self.oldModel), + self.tcombos, + self.getTemplateList(self.targetModel)) + + def _getMap(self, old=None, combos=None, new=None): + """A map from template's ord of the old model to template's ord of the new + model. Or None if no template + Contrary to what this name indicates, the method may be used + without templates. In getFieldMap it is used for fields + keywords parameter: + old -- the list of templates of the old model + combos -- the python list of gui's list of template + new -- the list of templates of the new model + If old is not given, the other two arguments are not used. + """ map = {} for i, f in enumerate(old): idx = combos[i].currentIndex() @@ -2110,10 +2169,10 @@ def getTemplateMap(self, old=None, combos=None, new=None): def getFieldMap(self): """Associating to each field's ord of the source model a field's ord (or None) of the new model.""" - return self.getTemplateMap( - old=self.oldModel['flds'], - combos=self.fcombos, - new=self.targetModel['flds']) + return self._getMap( + self.oldModel['flds'], + self.fcombos, + self.targetModel['flds']) def cleanup(self): """Actions to end this gui. diff --git a/aqt/preferences.py b/aqt/preferences.py index 32901cce08c..7fd2c832740 100644 --- a/aqt/preferences.py +++ b/aqt/preferences.py @@ -272,6 +272,7 @@ def updateOneOption(self, name, default=False, check=True, sync=True): storeValue[name] = value extraOptions = [ + ("preserveName", True), ] def setupExtra(self): diff --git a/designer/preferences.ui b/designer/preferences.ui index 0009a7bf405..b6995a67d69 100644 --- a/designer/preferences.ui +++ b/designer/preferences.ui @@ -7,7 +7,7 @@ 0 0 423 - 508 + 552 @@ -489,6 +489,16 @@ + + + + When changing a note's type, preserve name of fields/cards + + + true + + + diff --git a/difference.md b/difference.md index eeaf6f0f9f3..96a65f3808c 100644 --- a/difference.md +++ b/difference.md @@ -2,3 +2,11 @@ This files list the difference between regular anki and this forked version. It also lists the different options in the Preferences's extra page. +## When changing note type, preserve names (513858554) +If you change a note's type, and the old and the new note have +fields/cards with the same name, then those fields/cards are mapped by +default such that they keep the same name. + +In preferences, you can unbox "When changing note type, preserve names +if possible." to recover anki's default behavior, i.e. sending first +element to first one, second to second, etc.. From d78133caa415e6eea6078cf19a67bfcd6ef5c238 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Fri, 12 Jul 2019 06:24:34 +0200 Subject: [PATCH 56/68] Explain branches --- branchs.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 branchs.md diff --git a/branchs.md b/branchs.md new file mode 100644 index 00000000000..6aebf0b04c1 --- /dev/null +++ b/branchs.md @@ -0,0 +1,19 @@ +# List of branchs, and their meaning + +Here is what my different git branches are, and the way I intend to develop more features: + +intToConstant: same as Anki, but some integers are replaced by constant (i.e. variable). This make the code easier to read without changing it. I would love this to be incorporated into anki's main code, but Damien was note interested (and anyway, it's not ready yet, because I use f-string, and he can't use f-string) + +Commented: Same code as Anki, containing all changes from intToConstant, and containing a lot of comments. Some of you may already know this branch, since many people liked or forked my repo. This should help add-on developpers to understand Anki. It also contains documentation of many feature of Anki, which I shared with you on reddit in the past. + +baseFork: this branch contains everything done in Commented, plus everything needed by the fork. I.e. an Add-on class, documentation about how to add feature to Forked, etc...However, it contains no add-ons, thus it should behave as Anki. + +nameOfAnAddOn: there are plenty of branches of this kind. Each branch add a single feature to baseFork. This allow to test this feature alone. It also means that each time me (or another dev) want to add a feature, he can directly does it in a code looking like anki's code. + +fork: this branch merges all branches of the previous kind. I.e. it +contains all features. Most merge are easy to do, except when two +merges change the same method. However, merges are never totally +trivial, because each feature adds elements to some list, and git +needs help in order to know in which order to keep each element in the +list. For the sake of simplicity, I keep them in alphabetical order as +much as I can. From e93ddd4a3df703c39089b61bae5bb5f010f6f812 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Fri, 12 Jul 2019 06:24:40 +0200 Subject: [PATCH 57/68] Explain how to add add-ons in code --- How to add an add-on in the code.md | 126 ++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 How to add an add-on in the code.md diff --git a/How to add an add-on in the code.md b/How to add an add-on in the code.md new file mode 100644 index 00000000000..26c754578db --- /dev/null +++ b/How to add an add-on in the code.md @@ -0,0 +1,126 @@ +# How to add a feature to this fork. + +Create a branch, with parent baseFork. The name of this branch should +describe the feature you add. You then must modifies the code as +follow. See [branchs.md](branchs.md) to see what each branches are. + +## Addon folder + +In this folder, put a copy of the add-on, as it is in ankiweb when it +was added to this folder. + +This ensure that, if an add-on is updated, you can check easily what +was updated. What's new today, and thus not what is not yet +incorporated in the fork code. + +## differences.md + +List your add-on in the list of change between this fork and regular +anki. Add the add-on number after the title. + +Differences are listed in alphabetical order. + +## Change to code. +In order to be as compatible as possible with Anki, I respected the +following rules. And I'll ensure that any pull request (if by luck, +anyone is interested in contributing), satisfies the same rules: + +### Database +The database is not changed at all. Otherwise, it creates risk of +incompatibility with ankidroid, ios and ankiweb. However, I allow to +add element in json's dictionaries. + + +### Method return +All methods returns the same kind of values in the forked version and +in Anki. So that any add-on calling those methods will have the +expected result. + +As an example, imagine that anki's method `foo` returns an +int. Imagine that you want to return a pair, with the same int, and +also a Boolean. + +Then, you can rename this method as `fooAndBool`, return the +pair. Then you can defined `foo` as `fooAndBool()[0]`. + +### Method arguments. +All methods takes the same arguments. Any other arguments are keyword +argument. Most of the time, the default value could either ensure that +Anki is imitated, unless there is a good reason not to do it. + +### aqt/addons.py + +Add your add-on to the set of add-ons. + +## Configuration options + +If there are options to configure, add the buttons to +designer/preferences.ui + +A checkbox is done as follow: +```xml + + + + TEXT + + + +``` + +A text as follows: +```xml + + + + TEXT + + + true + + + +``` + +They should be added between +```xml + <html><head/><body><p><span style=" font-weight:600;">Extra</span><br/>Those options are not documented in anki's manual. They allow to configure the different add-ons incorporated in this special version of anki.</p></body></html> + + + true + + + +``` +and +```xml + + + + Qt::Vertical +``` + +#### Editing configuration +In `aqt.preferences` you should edit `extraOptions`, adding a tuple +(even if it contains a single value, it should be a tuple !), with the +following element: +* name of the widget in the profile manager, +* default value (False or 0 by default) +* whether it's a check box (True by default) +* whether this value should be synchronidez (True by default) + +Default values works as in any method call. + +You can access this value from anywhere in the code in two distinct +ways. If the value is not synchronized, it is in the profile manager, +and thus by: +```Python + from aqt import mw + mw.pm.profile.get("xMLName", DefaultValue) if mw else DefaultValue +``` +Note that, during test, `mw` is `None`. Otherwise, if the value is +synchronized, it is in the collection's configuration, and thus it is +accessed by +```Python + col.conf.get("xMLName", DefaultValue) +``` From 0c1ab345e92911c50d08e1456033f9672904c14b Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Fri, 12 Jul 2019 06:25:10 +0200 Subject: [PATCH 58/68] Edit Readme for the fork --- README.md | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 06f64a20e2b..910436e9e3a 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,33 @@ -Anki +# Forked Anki, Alpha version ------------------------------------- -This is the development branch of Anki. +This is the development branch of a forked version of Anki. For the official, please see [https://apps.ankiweb.net](https://apps.ankiweb.net). -For stable builds, please see https://apps.ankiweb.net. +## Description +This is a version of anki in which multiple add-ons are already incorporated. I only incorporated non-disruptive add-ons with respect to the way Anki works fundamentally. Examples of feature added include Copy notes, Batch edit, Exporting from browser, Postponing cards, Increasing speed of some operations, Keeping some long term back-up, Giving the name of cards which will be deleted instead of their index... For a complete list of change in Forked, please see [https://github.com/Arthur-Milchior/anki/blob/fork/difference.md](difference.md). -For non-developers who want to try this development code, -the easiest way is to use a binary package - please see -https://anki.tenderapp.com/discussions/beta-testing +I believe this fork is a crucial addition to Anki, because it allows you to have plenty of new and convinient features without having to wade through the list of all add-ons. Furthermore, it deals with the issue of incompatibility between add-ons, which can be difficult to manage. Finally, it also allows for graphical configuration instead of a json-edit based one. -To run from source, please see README.development. +### Add-on compatibility +This fork is theoritically equivalent to anki and should therefore be fully compatible with any add-ons. Any already installed add-ons that is being installed again will simply be ignored + +### Alpha specificity +This fork is still in alpha and in a very early stage. This mean it is not as ready-to-use as I would like - you need to download the github code and execute runanki on it instead of having an archive ready, as of now. +If you want to be an alpha-tester, please do notify me, I am still in need for some. + +## Notes for devs: +### Why fork is easier than add-on +Honestly, I find that incorporating code in Anki is far easier than add-ons. Because: +* I often need to monkey patch a method by copying anki's method, adding it in an add-on, and changing a single line. This is a problem because it means I have copied the method from Anki and am detached from its code - if Anki does change the method the changes are not pushed through. This leads to unexpected bugs that are hard to track down, and that only appears when Anki is updated to a newer version. By using a fork, and merging regularly Anki with the forked version, this is no longer an issue. +* There is no problem of add-on compatibility. Indeed, if many add-ons change the same method, I can just do all the change in one file, instead of having to find an arcane way to merge them. Currently with add-ons, I have to create yet another add-on, merging the many incompatible add-ons in a new one. This is what I did, for example, when I created an add-on merging «multiple column editor» and «frozen fields», which is not an elegant solution. +* Some methods are extremely lengthy, such as ``fixintegrity`` (i.e. "Check database"). With a fork, I can now split this method in many smaller ones. +* I can use anki's preference window to configure everything, instead of having to relies on json files or in specialized configuration windows. -If you are interested in contributing changes to Anki, please -see README.contributing before you begin work. +### How to add features +For a detailled explanation, see [How to add an add-on in the code.md](How to add an add-on in the code) + +### Branches + +To run from source, please see README.development. -[![Build Status](https://travis-ci.org/dae/anki.svg?branch=master)](https://travis-ci.org/dae/anki) +If you are interested in contributing changes to this Fork, just contact arthur@milchior.fr From 705825657fe290dacfb6691d0e2a4dd3a7bed720 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Mon, 17 Jun 2019 14:19:04 +0200 Subject: [PATCH 59/68] Explodefixintegrity --- addons/1135180054/LICENSE | 674 ++++++++++++++++++++++++++++++++++ addons/1135180054/README.md | 79 ++++ addons/1135180054/__init__.py | 5 + addons/1135180054/check.py | 51 +++ addons/1135180054/config.json | 1 + addons/1135180054/config.py | 15 + addons/1135180054/fix.py | 204 ++++++++++ addons/1135180054/sync.py | 78 ++++ anki/collection.py | 366 +++++++++++------- aqt/addons.py | 1 + aqt/preferences.py | 1 + aqt/sync.py | 11 +- designer/preferences.ui | 16 +- difference.md | 11 + 14 files changed, 1380 insertions(+), 133 deletions(-) create mode 100755 addons/1135180054/LICENSE create mode 100755 addons/1135180054/README.md create mode 100755 addons/1135180054/__init__.py create mode 100755 addons/1135180054/check.py create mode 100755 addons/1135180054/config.json create mode 100755 addons/1135180054/config.py create mode 100755 addons/1135180054/fix.py create mode 100755 addons/1135180054/sync.py diff --git a/addons/1135180054/LICENSE b/addons/1135180054/LICENSE new file mode 100755 index 00000000000..94a9ed024d3 --- /dev/null +++ b/addons/1135180054/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/addons/1135180054/README.md b/addons/1135180054/README.md new file mode 100755 index 00000000000..fdebf98e00e --- /dev/null +++ b/addons/1135180054/README.md @@ -0,0 +1,79 @@ +# Database checker/fixer explained, more fixers +## Rationale +This add-on simultaneously solve two related problems. + +### Meaningful error message +Sometime, anki warns you that your database has trouble. That you +should run «Check database» to correct it. I find this quite +frustrating, because I've no clue what's wrong. And since it may +happens many time, I would like to understand what's wrong to avoid +repeating the error. This is especially important for add-on +developpers, because if our add-ons break the database, it would be +better to know exactly how it breaks, so that we can correct +this. Hence, this add-on change message sent by the methods used to +check and to fix the database, so that it explains exactly what it +does. + +More information on the error messages can be found on +https://github.com/Arthur-Milchior/anki/blob/master/documentation/DB_Check.md + +### Checking what anki forgot to check +There are kind(s) of error(s) that anki does not check. This add-on +also check and correct those kind of errors. The following error(s) +are currently considered: * Whether you have a note which has two +distinct cards for the same card type. If this is the case, it keeps +the card with greatest interval. In case of equality, it keeps the one +with greatest easyness. In case of equality the one with greates due +date. Otherwise, an arbitrary card. I explain in [the +forum](https://anki.tenderapp.com/discussions/ankidesktop/32854-two-cards-of-the-same-note-with-same-nid#comment_47016398) +how this could occur without having a single add-on installed. + + +## Warning +This add-on is incompatible with add-on +* [12287769](https://ankiweb.net/shared/info/12287769) «Explain +deletion». This add-on ensure that the file `deleted.txt` state +why notes are deleted. +* [Quicker Anki: 802285486](https://ankiweb.net/shared/info/802285486) + which makes some operation quicker. + + +Please instead use-addon +[777545149](https://ankiweb.net/shared/info/777545149) which merges +those three add-ons + + +With one exception, this add-on does not actually edit the +database. All editions are done by anki original code. The exception +being the check which are done by this add-on and not by anki. Right +now, it's only: checking whether a note has a duplicate card. + +## Internal +In the class `anki.collection._Collection`, it changes the +methods: +* `basicCheck`, a method used thrice during sync to check whether +everything is ok or whether there is an inconsistency. +* `fixIntegrity`, the method which is used when clicking on "Check +Database". +* `_checkFailed`. The new method explain the problem comes from + the server. + +## Version 2.0 +None + +## TODO +Use config to allow user to choose to have only one of the two +functionnality. Allow to choose which card to keep. + +Make it compatible with add-on mentionned above. + +## Links, licence and credits + +Key |Value +------------|------------------------------------------------------------------- +Copyright | Arthur Milchior +Based on | Anki code by Damien Elmes +License | GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +Source in | https://github.com/Arthur-Milchior/anki-database-check-explained +Addon number| [1135180054](https://ankiweb.net/shared/info/1135180054) +Support me on| [![Ko-fi](https://ko-fi.com/img/Kofi_Logo_Blue.svg)](Ko-fi.com/arthurmilchior) or [![Patreon](http://www.milchior.fr/patreon.png)](https://www.patreon.com/bePatron?u=146206) diff --git a/addons/1135180054/__init__.py b/addons/1135180054/__init__.py new file mode 100755 index 00000000000..8ca3cf6073d --- /dev/null +++ b/addons/1135180054/__init__.py @@ -0,0 +1,5 @@ +from . import check +from . import fix +from . import test +from . import sync +from . import main diff --git a/addons/1135180054/check.py b/addons/1135180054/check.py new file mode 100755 index 00000000000..f5e38016b35 --- /dev/null +++ b/addons/1135180054/check.py @@ -0,0 +1,51 @@ +from anki.collection import _Collection +from anki.consts import * +from anki.utils import ids2str +import sys + +def basicCheck(self): + """True if basic integrity is meet. + + Used before and after sync, or before a full upload. + + Tests: + * whether each card belong to a note + * each note has a model + * each note has a card + * each card's ord is valid according to the note model. + * each card has distinct pair (ord, nid) + + """ + checks = [ + ("select id, nid from cards where nid not in (select id from notes)", + "Card {} belongs to note {} which does not exists"), + ("select id, flds, tags, mid from notes where id not in (select distinct nid from cards)", + """Note {} has no cards. Fields: «{}», tags:«{}», mid:«{}»"""), + ("""select id, flds, tags, mid from notes where mid not in %s""" % ids2str(self.models.ids()), + """Note {} has an unexisting note type. Fields: «{}», tags:«{}», mid:{}"""), + ("""select nid, ord, count(*), GROUP_CONCAT(id) from cards group by ord, nid having count(*)>1""", + """Note {} has card at ord {} repeated {} times. Card ids are {}""" + ) + ] + for m in self.models.all(): + # ignore clozes + mid = m['id'] + if m['type'] != MODEL_STD: + continue + checks.append((f"""select id, ord, nid from cards where ord <0 or ord>{len(m['tmpls'])} and nid in (select id from notes where mid = {mid})""", + "Card {}'s ord {} of note {} does not exists in model {mid}")) + + error = False + for query,msg in checks: + l = self.db.all(query) + for tup in l: + #print(f"Message is «{msg}», tup = «{tup}»", file = sys.stderr) + formatted = msg.format(*tup) + print(formatted, file = sys.stderr) + error = True + + if error: + return + return True + +_Collection.basicCheck = basicCheck diff --git a/addons/1135180054/config.json b/addons/1135180054/config.json new file mode 100755 index 00000000000..6a99c569c32 --- /dev/null +++ b/addons/1135180054/config.json @@ -0,0 +1 @@ +{"Create empty card for empty note":true} diff --git a/addons/1135180054/config.py b/addons/1135180054/config.py new file mode 100755 index 00000000000..1746f18c947 --- /dev/null +++ b/addons/1135180054/config.py @@ -0,0 +1,15 @@ +from aqt import mw +userOption = None +def getUserOption(key = None, default = None): + global userOption + if userOption is None: + userOption = mw.addonManager.getConfig(__name__) + if key is not None: + return userOption.get(key, default) + return userOption + +def update(_): + global userOption + userOption = None + +mw.addonManager.setConfigUpdatedAction(__name__,update) diff --git a/addons/1135180054/fix.py b/addons/1135180054/fix.py new file mode 100755 index 00000000000..8027cd74a83 --- /dev/null +++ b/addons/1135180054/fix.py @@ -0,0 +1,204 @@ +from anki.collection import _Collection +from anki.consts import * +from anki.lang import _, ngettext +from anki.utils import ids2str, intTime +from aqt import mw +from anki.cards import Card +from .config import getUserOption +import os +import stat + +def integrity(self): + if self.db.scalar("pragma integrity_check") != "ok": + return (_("The database containing the collection is corrupt (i.e. pragma integrity_check fails). Please see the manual."), False) + +def fixOverride(self, problems): + for m in self.models.all(): + for t in m['tmpls']: + if t['did'] == "None": + problems.append(_("Fixed AnkiDroid deck override bug. (I.e. some template has did = to 'None', it is now None.)")) + +def template(query, problem, singular, plural): + def f(self,problems): + l = self.db.all(query) + for tup in l: + problems.append(problem.format(*tup)) + if l: + problems.append(ngettext(singular, + plural, len(l)) + % len(l)) + return f + +def noteWithMissingModel(self, problems): + (template( + """select id, flds, tags, mid from notes where mid not in """ + ids2str(self.models.ids()), + "Deleted note {}, with fields «{}», tags «{}» whose model id is {}, which does not exists.", + "Deleted %d note with missing note type.", + "Deleted %d notes with missing note type." + ))(self, problems) + +def fixInvalidCardOrdinal(self, problems): + funs = [ + template( + f"""select id, nid, ord from cards where (ord <0 or ord >= {len(m['tmpls'])}) and nid in (select id from notes where mid = {m['id']})""", + "Deleted card {} of note {} because its ord {} does not belong to its model", + "Deleted %d card with missing template.", + "Deleted %d cards with missing template.") + for m in self.models.all() if m['type'] == MODEL_STD] + for fun in funs: + fun(self,problems) + +def fixWrongNumberOfField(self, problems): + for m in self.models.all(): + # notes with invalid field count + l = self.db.all( + "select id, flds from notes where mid = ?", m['id']) + ids = [] + for id, flds in l: + nbFieldNote = flds.count("\x1f") + 1 + nbFieldModel = len(m['flds']) + if nbFieldNote != nbFieldModel: + ids.append(id) + problems.append(f"""Note {id} with fields «{flds}» has {nbFieldNote} fields while its model {m['name']} has {nbFieldModel} fields""") + + +def fixNoteWithoutCard(self, problems): + if getUserOption("Create empty card for empty note"): + l = self.db.all("""select id, flds, tags, mid from notes where id not in (select distinct nid from cards)""") + for nid, flds, tags, mid in l: + note = mw.col.getNote(nid) + note.addTag("NoteWithNoCard") + note.flush() + model = note.model() + template0 = model["tmpls"][0] + mw.col._newCard(note,template0,mw.col.nextID("pos")) + problems.append("No cards in note {} with fields «{}» and tags «{}» of model {}. Adding card 1 and tag «NoteWithNoCard».".format(nid, fld, tags, mid)) + if l: + problems.append("Created %d cards for notes without card"% (len(l))) + else: + (template( + """select id, flds, tags, mid from notes where id not in (select distinct nid from cards)""", + "Deleting note {} with fields «{}» and tags «{}» of model {} because it has no card.", + "Deleted %d note with no cards.", + "Deleted %d notes with no cards."))(self, problems) + +def fixCardWithoutNote(self, problems): + (template( + "select id, nid from cards where nid not in (select id from notes)", + "Deleted card {} of note {} because this note does not exists.", + "Deleted %d card with missing note.", + "Deleted %d cards with missing note."))(self, problems) + +def fixOdueType1(self, problems): + (template( + "select id,nid from cards where odue > 0 and type=1 and not odid", + "set odue of card {} of note {} to 0, because it was positive while type was 1 (i.e. card in learning)", + "Fixed %d card with invalid properties.", + "Fixed %d cards with invalid properties."))(self, problems) + +def fixOdueQueue2(self, problems): + (template( + "select id, nid from cards where odue > 0 and queue=2 and not odid", + "set odue of card {} of note {} to 0, because it was positive while queue was 2 (i.e. in the review queue).", + "Fixed %d card with invalid properties.", + "Fixed %d cards with invalid properties.") + )(self, problems) + +def fixOdidOdue(self, problems): + (template( + """select id, odid, did from cards where odid > 0 and did in %s""" % ids2str([id for id in self.decks.allIds() if not self.decks.isDyn(id)]),# cards with odid set when not in a dyn deck + "Card {}: Set odid and odue to 0 because odid was {} while its did was {} which is not filtered(a.k.a. not dymanic).", + "Fixed %d card with invalid properties.", + "Fixed %d cards with invalid properties.") + )(self, problems) + +def atMost1000000Due(self, problems): + # new cards can't have a due position > 32 bits + (template ("""select cards, due where due > 1000000 and type = 0""", + "Card {}: set due to 1000000 because it was {}, which is far too big for a due card.", + "Fixed %d due card with due too big.", + "Fixed %d due cards with due too big." + ))(self, problems) + + +def setNextPos(self): + # new card position + self.conf['nextPos'] = self.db.scalar( + "select max(due)+1 from cards where type = 0") or 0 + +def reasonableRevueDue(self, problems): + (template(# reviews should have a reasonable due # + "select id, due from cards where queue = 2 and due > 100000", + "Changue of card {}, because its due is {} which is excessive", + "Reviews had incorrect due date.", + "Reviews had incorrect due date." + ) + )(self, problems) + +# v2 sched had a bug that could create decimal intervals +def fixFloatIvl(self, problems): + (template( + "select id, ivl from cards where ivl != round(ivl)", + "Round the ivl of card {} because it was {} (this is a known bug of schedule v2.", + "Fixed %d cards with v2 scheduler bug.", + "Fixed %d cards with v2 scheduler bug." + ) + )(self, problems) + +def fixFloatDue(self, problems): + (template( + "select id, due from cards where due != round(due)", + "Round the due of card {} because it was {} (this is a known bug of schedule v2.", + "Fixed %d cards with v2 scheduler bug.", + "Fixed %d cards with v2 scheduler bug."))(self, problems) + +def doubleCard(self, problems): + l = self.db.all("""select nid, ord, count(*), GROUP_CONCAT(id) from cards group by ord, nid having count(*)>1""") + toRemove = [] + for nid, ord, count, ids in l: + ids = ids.split(",") + ids = [int(id) for id in ids] + cards = [Card(self,id) for id in ids] + bestCard = max(cards, key = (lambda card: (card.ivl, card.factor, card.due))) + bestId = bestCard.id + problems.append(f"There are {count} card for note {nid} at ord {ord}. They are {ids}. We only keep {bestId}") + toRemove += [id for id in ids if id!= bestId] + if toRemove: + self.remCards(toRemove) + + +oldFixIntegrity = _Collection.fixIntegrity +def fixIntegrity(self): + """Find the problems which will be found. Then call last fixing.""" + problems = [] + self.save() + ret = integrity(self)#If the database itself is broken, there is nothing else to do. + if ret: + return ret + + for fun in [noteWithMissingModel, + fixOverride, + fixInvalidCardOrdinal, + fixWrongNumberOfField, + fixNoteWithoutCard, + fixCardWithoutNote, + fixOdueType1, + fixOdueQueue2, + fixOdidOdue, + reasonableRevueDue, + fixFloatIvl, + fixFloatDue, + doubleCard, + ]: + fun(self,problems) + # tags + # self.tags.registerNotes() + # # field cache + # for m in self.models.all(): + # self.updateFieldCache(self.models.nids(m)) + + oldProblems, ok = oldFixIntegrity(self) + problems.append(oldProblems) + return ("\n".join(problems), ok) + +_Collection.fixIntegrity = fixIntegrity diff --git a/addons/1135180054/sync.py b/addons/1135180054/sync.py new file mode 100755 index 00000000000..b1467c71485 --- /dev/null +++ b/addons/1135180054/sync.py @@ -0,0 +1,78 @@ +from aqt.sync import SyncManager +from anki.lang import _ +from aqt.utils import showWarning, showText, tooltip + +def _checkFailed(self, event): + showWarning(_(f"""\ +Your collection is in an inconsistent state. Please run Tools>\ +Check Database, then sync again. +This is due to event {event} during sync.""")) +SyncManager._checkFailed = _checkFailed + +def onEvent(self, evt, *args): + pu = self.mw.progress.update + if evt == "badAuth": + tooltip( + _("AnkiWeb ID or password was incorrect; please try again."), + parent=self.mw) + # blank the key so we prompt user again + self.pm.profile['syncKey'] = None + self.pm.save() + elif evt == "corrupt": + pass + elif evt == "newKey": + self.pm.profile['syncKey'] = args[0] + self.pm.save() + elif evt == "offline": + tooltip(_("Syncing failed; internet offline.")) + elif evt == "upbad": + self._didFullUp = False + self._checkFailed("upbad") + elif evt == "sync": + m = None; t = args[0] + if t == "login": + m = _("Syncing...") + elif t == "upload": + self._didFullUp = True + m = _("Uploading to AnkiWeb...") + elif t == "download": + m = _("Downloading from AnkiWeb...") + elif t == "sanity": + m = _("Checking...") + elif t == "findMedia": + m = _("Checking media...") + elif t == "upgradeRequired": + showText(_("""\ +Please visit AnkiWeb, upgrade your deck, then try again.""")) + if m: + self.label = m + self._updateLabel() + elif evt == "syncMsg": + self.label = args[0] + self._updateLabel() + elif evt == "error": + self._didError = True + showText(_("Syncing failed:\n%s")% + self._rewriteError(args[0])) + elif evt == "clockOff": + self._clockOff() + elif evt == "checkFailed": + self._checkFailed("checkFailed") + elif evt == "mediaSanity": + showWarning(_("""\ +A problem occurred while syncing media. Please use Tools>Check Media, then \ +sync again to correct the issue.""")) + elif evt == "noChanges": + pass + elif evt == "fullSync": + self._confirmFullSync() + elif evt == "downloadClobber": + showInfo(_("Your AnkiWeb collection does not contain any cards. Please sync again and choose 'Upload' instead.")) + elif evt == "send": + # posted events not guaranteed to arrive in order + self.sentBytes = max(self.sentBytes, int(args[0])) + self._updateLabel() + elif evt == "recv": + self.recvBytes = max(self.recvBytes, int(args[0])) + self._updateLabel() +SyncManager.onEvent = onEvent diff --git a/anki/collection.py b/anki/collection.py index cc0f7b94c49..adee4b49c8d 100644 --- a/anki/collection.py +++ b/anki/collection.py @@ -937,170 +937,288 @@ def basicCheck(self): * each card's ord is valid according to the note model. ooo """ - # cards without notes - if self.db.scalar(""" -select 1 from cards where nid not in (select id from notes) limit 1"""): - return - # notes without cards or models - if self.db.scalar(""" -select 1 from notes where id not in (select distinct nid from cards) -or mid not in %s limit 1""" % ids2str(self.models.ids())): - return - # invalid ords + checks = [ + ("select id, nid from cards where nid not in (select id from notes)", + "Card {} belongs to note {} which does not exists"), + ("select id, flds, tags, mid from notes where id not in (select distinct nid from cards)", + """Note {} has no cards. Fields: «{}», tags:«{}», mid:«{}»"""), + ("""select id, flds, tags, mid from notes where mid not in %s""" % ids2str(self.models.ids()), + """Note {} has an unexisting note type. Fields: «{}», tags:«{}», mid:{}"""), + ("""select nid, ord, count(*), GROUP_CONCAT(id) from cards group by ord, nid having count(*)>1""", + """Note {} has card at ord {} repeated {} times. Card ids are {}""" + ) + ] for m in self.models.all(): # ignore clozes + mid = m['id'] if m['type'] != MODEL_STD: continue - if self.db.scalar(""" -select 1 from cards where ord not in %s and nid in ( -select id from notes where mid = ?) limit 1""" % - ids2str([t['ord'] for t in m['tmpls']]), - m['id']): - return + checks.append((f"""select id, ord, nid from cards where ord <0 or ord>{len(m['tmpls'])} and nid in (select id from notes where mid = {mid})""", + "Card {}'s ord {} of note {} does not exists in model {mid}")) + error = False + for query,msg in checks: + l = self.db.all(query) + for tup in l: + #print(f"Message is «{msg}», tup = «{tup}»", file = sys.stderr) + formatted = msg.format(*tup) + print(formatted, file = sys.stderr) + error = True + if error: + return return True + """List of method to call to fix things. It can be edited by add-on, in order to add more method, or delete/change some. """ + listFix = ["noteWithMissingModel", + "fixOverride", + "fixReq", + "fixInvalidCardOrdinal", + "fixWrongNumberOfField", + "fixNoteWithoutCard", + "fixCardWithoutNote", + "fixOdueType1", + "fixOdueQueue2", + "fixOdidOdue", + "intermediate", + "reasonableRevueDue", + "fixFloatIvlInCard", + "fixFloatIvlInRevLog", + "fixFloatDue", + "doubleCard", + "ensureSomeNoteType", + ] + + def fixIntegrity(self): - "Fix possible problems and rebuild caches." - problems = [] - curs = self.db.cursor() + """Find the problems which will be found. Then call last fixing.""" + #differences: not recomputing models' req. Giving reason to deletion + self.problems = [] + self.curs = self.db.cursor() + self.save() + ret = self.integrity()#If the database itself is broken, there is nothing else to do. + if ret: + return ret + + for f in self.listFix:#execute all methods required to fix something + getattr(self,f)() + # whether sqlite find a problem in its database + self.optimize() + newSize = os.stat(self.path)[stat.ST_SIZE] + txt = _("Database rebuilt and optimized.") + ok = not self.problems + self.problems.append(txt) + # if any self.problems were found, force a full sync + if not ok: + self.modSchema(check=False) self.save() - oldSize = os.stat(self.path)[stat.ST_SIZE] + return ("\n".join(self.problems), ok) + def integrity(self): # whether sqlite find a problem in its database if self.db.scalar("pragma integrity_check") != "ok": return (_("Collection is corrupt. Please see the manual."), False) - # note types with a missing model - ids = self.db.list(""" -select id from notes where mid not in """ + ids2str(self.models.ids())) - if ids: - problems.append( - ngettext("Deleted %d note with missing note type.", - "Deleted %d notes with missing note type.", len(ids)) - % len(ids)) - self.remNotes(ids) - - # for each model + def template(self, query, singular, plural, deletion = None, reason = None): + """Execute query. If it returns some value, it means there was a + problem. Append it to self.problems. Apply deletion to the + list returned by the query to correct the error. + + reason -- not used currently + """ + l = self.db.all(query) + for tup in l: + self.problems.append(problem.format(*tup)) + if l: + self.problems.append(ngettext(singular, + plural, len(l)) + % len(l)) + if deletion: + deletion(l) + + def noteWithMissingModel(self): + self.template(""" + select id, flds, tags, mid from notes where mid not in """ + ids2str(self.models.ids()), + "Deleted note {}, with fields «{}», tags «{}» whose model id is {}, which does not exists.", + "Deleted %d note with missing note type.", + "Deleted %d notes with missing note type.", + lambda lines: self.remNotes([line[0]for line in lines], reason=f"Its note type is missing" + ) + ) + + def fixOverride(self): for m in self.models.all(): for t in m['tmpls']: if t['did'] == "None": t['did'] = None - problems.append(_("Fixed AnkiDroid deck override bug.")) + self.problems.append(_("Fixed AnkiDroid deck override bug. (I.e. some template has did = to 'None', it is now None.)")) self.models.save(m) + + def fixReq(self): + for m in self.models.all(): if m['type'] == MODEL_STD: # model with missing req specification if 'req' not in m: self.models._updateRequired(m) - problems.append(_("Fixed note type: %s") % m['name']) - # cards with invalid ordinal - ids = self.db.list(""" -select id from cards where ord not in %s and nid in ( -select id from notes where mid = ?)""" % - ids2str([t['ord'] for t in m['tmpls']]), - m['id']) - if ids: - problems.append( - ngettext("Deleted %d card with missing template.", - "Deleted %d cards with missing template.", - len(ids)) % len(ids)) - self.remCards(ids) + self.problems.append(_("Fixed note type: %s") % m['name']) + + def fixInvalidCardOrdinal(self): + for m in self.models.all(): + if m['type'] == MODEL_STD: + self.template( + f"""select id, nid, ord from cards where (ord <0 or ord >= {len(m['tmpls'])}) and nid in (select id from notes where mid = {m['id']})""", + "Deleted card {} of note {} because its ord {} does not belong to its model", + "Deleted %d card with missing template.", + "Deleted %d cards with missing template.", + lambda lines: self.remCards([line[0]for line in lines], reason=f"Cards removed because of missing templates." + ) + ) + + def fixWrongNumberOfField(self): + for m in self.models.all(): # notes with invalid field count + l = self.db.all( + "select id, flds from notes where mid = ?", m['id']) ids = [] - for id, flds in self.db.execute( - "select id, flds from notes where mid = ?", m['id']): - if (flds.count("\x1f") + 1) != len(m['flds']): + for (id, flds) in l: + nbFieldNote = flds.count("\x1f") + 1 + nbFieldModel = len(m['flds']) + if nbFieldNote != nbFieldModel: ids.append(id) + self.problems.append(f"""Note {id} with fields «{flds}» has {nbFieldNote} fields while its model {m['name']} has {nbFieldModel} fields""") if ids: - problems.append( - ngettext("Deleted %d note with wrong field count.", - "Deleted %d notes with wrong field count.", - len(ids)) % len(ids)) - self.remNotes(ids) - # delete any notes with missing cards - ids = self.db.list(""" -select id from notes where id not in (select distinct nid from cards)""") - if ids: - cnt = len(ids) - problems.append( - ngettext("Deleted %d note with no cards.", - "Deleted %d notes with no cards.", cnt) % cnt) - self._remNotes(ids) - # cards with missing notes - ids = self.db.list(""" -select id from cards where nid not in (select id from notes)""") - if ids: - cnt = len(ids) - problems.append( - ngettext("Deleted %d card with missing note.", - "Deleted %d cards with missing note.", cnt) % cnt) - self.remCards(ids) - # cards with odue set when it shouldn't be - ids = self.db.list(f""" -select id from cards where odue > 0 and (type={CARD_LRN} or queue={CARD_DUE}) and not odid""") - if ids: - cnt = len(ids) - problems.append( - ngettext("Fixed %d card with invalid properties.", - "Fixed %d cards with invalid properties.", cnt) % cnt) - self.db.execute("update cards set odue=0 where id in "+ - ids2str(ids)) - # cards with odid set when not in a dyn deck - dids = [id for id in self.decks.allIds() if not self.decks.isDyn(id)] - ids = self.db.list(""" - select id from cards where odid > 0 and did in %s""" % ids2str(dids)) - if ids: - cnt = len(ids) - problems.append( - ngettext("Fixed %d card with invalid properties.", - "Fixed %d cards with invalid properties.", cnt) % cnt) - self.db.execute("update cards set odid=0, odue=0 where id in "+ - ids2str(ids)) + self.remNotes([line[0]for line in lines],reason=f"It hads a wrong number of fields") + + + def fixNoteWithoutCard(self): + from aqt import mw + noteWithoutCard = self.config.get("noteWithoutCard", True) + if noteWithoutCard: + l = self.db.all("""select id, flds, tags, mid from notes where id not in (select distinct nid from cards)""") + for nid, flds, tags, mid in l: + note = mw.col.getNote(nid) + note.addTag("NoteWithNoCard") + note.flush() + model = note.model() + template0 = model["tmpls"][0] + mw.col._newCard(note,template0,mw.col.nextID("pos")) + self.problems.append("No cards in note {} with fields «{}» and tags «{}» of model {}. Adding card 1 and tag «NoteWithNoCard».".format(nid, fld, tags, mid)) + if l: + self.problems.append("Created %d cards for notes without card"% (len(l))) + else: + self.template( + """select id, flds, tags, mid from notes where id not in (select distinct nid from cards)""", + "Deleting note {} with fields «{}» and tags «{}» of model {} because it has no card.", + "Deleted %d note with no cards.", + "Deleted %d notes with no cards.", + lambda lines: self.remNotes([line[0]for line in lines],reason=f"It has no cards") + ) + + def fixCardWithoutNote(self): + self.template( + "select id, nid from cards where nid not in (select id from notes)", + "Deleted card {} of note {} because this note does not exists.", + "Deleted %d card with missing note.", + "Deleted %d cards with missing note.", + lambda lines: self.remCards([line[0]for line in lines],reason=f"Card's note is missing") + ) + + def fixOdueType1(self): + # cards with odue set when it shouldn't be + self.template( + f"select id,nid from cards where odue > 0 and type={CARD_LRN} and not odid", + "set odue of card {} of note {} to 0, because it was positive while type was 1 (i.e. card in learning)", + "Fixed %d card with invalid properties.", + "Fixed %d cards with invalid properties.", + lambda lines:(self.db.execute("update cards set odue=0 where id in "+ids2str([line[0] for line in lines])))) + + def fixOdueQueue2(self): + self.template( + f"select id, nid from cards where odue > 0 and queue={CARD_DUE} and not odid", + "set odue of card {} of note {} to 0, because it was positive while queue was 2 (i.e. in the review queue).", + "Fixed %d card with invalid properties.", + "Fixed %d cards with invalid properties.", + lambda lines:(self.db.execute("update cards set odue=0 where id in "+ids2str([line[0] for line in lines])))) + + + + def fixOdidOdue(self): + self.template( + """select id, odid, did from cards where odid > 0 and did in %s""" % ids2str([id for id in self.decks.allIds() if not self.decks.isDyn(id)]),# cards with odid set when not in a dyn deck + "Card {}: Set odid and odue to 0 because odid was {} while its did was {} which is not filtered(a.k.a. not dymanic).", + "Fixed %d card with invalid properties.", + "Fixed %d cards with invalid properties.", + lambda lists: self.db.execute("update cards set odid=0, odue=0 where id in "+ + ids2str([list[0] for list in lists])) + ) + + def intermediate(self): # tags self.tags.registerNotes() # field cache for m in self.models.all(): self.updateFieldCache(self.models.nids(m)) - # new cards can't have a due position > 32 bits, so wrap items over - # 2 million back to 1 million - curs.execute(f""" -update cards set due=1000000+due%1000000,mod=?,usn=? where due>=1000000 -and type = {CARD_NEW}""", [intTime(), self.usn()]) - if curs.rowcount: - problems.append("Found %d new cards with a due number >= 1,000,000 - consider repositioning them in the Browse screen." % curs.rowcount) + + def atMost1000000Due(self): + # new cards can't have a due position > 32 bits + curs = self.db.cursor() + curs.execute(""" + update cards set due = 1000000, mod = ?, usn = ? where due > 1000000 + and type = {CARD_NEW}""", intTime(), self.usn()) + if curs.rowcount: + self.problems.append("Fixed %d cards with due greater than 1000000 due." % curs.rowcount) + + def setNextPos(self): # new card position self.conf['nextPos'] = self.db.scalar( - f"select max(due)+1 from cards where type = {CARD_NEW}") or 0 - # reviews should have a reasonable due # - ids = self.db.list( - "select id from cards where queue = 2 and due > 100000") - if ids: - problems.append("Reviews had incorrect due date.") - self.db.execute( + "select max(due)+1 from cards where type = 0") or 0 + + def reasonableRevueDue(self): + self.template(# reviews should have a reasonable due # + "select id, due from cards where queue = 2 and due > 100000", + "Changue of card {}, because its due is {} which is excessive", + "Reviews had incorrect due date.", + "Reviews had incorrect due date.", + lambda lists: self.db.execute( "update cards set due = ?, ivl = 1, mod = ?, usn = ? where id in %s" - % ids2str(ids), self.sched.today, intTime(), self.usn()) - # v2 sched had a bug that could create decimal intervals + % ids2str([list[0] for list in lists]), self.sched.today, intTime(), self.usn()) + ) + + # v2 sched had a bug that could create decimal intervals + def fixFloatIvlInCard(self): + curs = self.db.cursor() curs.execute("update cards set ivl=round(ivl),due=round(due) where ivl!=round(ivl) or due!=round(due)") if curs.rowcount: - problems.append("Fixed %d cards with v2 scheduler bug." % curs.rowcount) + self.problems.append("Fixed %d cards with v2 scheduler bug." % curs.rowcount) + def fixFloatIvlInRevLog(self): + curs = self.db.cursor() curs.execute("update revlog set ivl=round(ivl),lastIvl=round(lastIvl) where ivl!=round(ivl) or lastIvl!=round(lastIvl)") if curs.rowcount: - problems.append("Fixed %d review history entries with v2 scheduler bug." % curs.rowcount) - # models + self.problems.append("Fixed %d review history entires with v2 scheduler bug." % curs.rowcount) + + def fixFloatDue(self): + self.template( + "select id, due from cards where due != round(due)", + "Round the due of card {} because it was {} (this is a known bug of schedule v2.", + "Fixed %d cards with v2 scheduler bug.", + "Fixed %d cards with v2 scheduler bug.") + + def doubleCard(self): + l = self.db.all("""select nid, ord, count(*), GROUP_CONCAT(id) from cards group by ord, nid having count(*)>1""") + toRemove = [] + for nid, ord, count, ids in l: + ids = ids.split(",") + ids = [int(id) for id in ids] + cards = [Card(self,id) for id in ids] + bestCard = max(cards, key = (lambda card: (card.ivl, card.factor, card.due))) + bestId = bestCard.id + self.problems.append(f"There are {count} card for note {nid} at ord {ord}. They are {ids}. We only keep {bestId}") + toRemove += [id for id in ids if id!= bestId] + if toRemove: + self.remCards(toRemove) + + def ensureSomeNoteType(self): if self.models.ensureNotEmpty(): - problems.append("Added missing note type.") - # and finally, optimize - self.optimize() - newSize = os.stat(self.path)[stat.ST_SIZE] - txt = _("Database rebuilt and optimized.") - ok = not problems - print("Adding in collection.py") - problems.append(txt) - # if any problems were found, force a full sync - if not ok: - self.modSchema(check=False) - self.save() - return ("\n".join(problems), ok) + self.problems.append("Added missing note type.") def optimize(self): """Tell sqlite to optimize the db""" diff --git a/aqt/addons.py b/aqt/addons.py index 70275bee3db..70805b87a28 100644 --- a/aqt/addons.py +++ b/aqt/addons.py @@ -901,6 +901,7 @@ def __hash__(self): """ Set of characteristic of Add-ons incorporated here""" incorporatedAddonsSet = { + Addon("«Check database» Explain errors and what is done to fix it", 1135180054, gitHash = "371c360e5611ad3eec5dcef400d969e7b1572141", gitRepo = "https://github.com/Arthur-Milchior/anki-database-check-explained"), #mod unkwon because it's not directly used by the author anymore } incorporatedAddonsDict = {**{addon.name: addon for addon in incorporatedAddonsSet if addon.name}, diff --git a/aqt/preferences.py b/aqt/preferences.py index 32901cce08c..46fd3d87a4a 100644 --- a/aqt/preferences.py +++ b/aqt/preferences.py @@ -272,6 +272,7 @@ def updateOneOption(self, name, default=False, check=True, sync=True): storeValue[name] = value extraOptions = [ + ("noteWithoutCard", True), ] def setupExtra(self): diff --git a/aqt/sync.py b/aqt/sync.py index 2f3c24b2a74..2a230693c64 100644 --- a/aqt/sync.py +++ b/aqt/sync.py @@ -19,8 +19,6 @@ class SyncManager(QObject): """ label -- The current action done by the synchronizez. Or in case f syncMsg, the first argument """ - - def __init__(self, mw, pm): QObject.__init__(self, mw) self.mw = mw @@ -107,7 +105,7 @@ def onEvent(self, evt, *args): tooltip(_("Syncing failed; internet offline.")) elif evt == "upbad": self._didFullUp = False - self._checkFailed() + self._checkFailed("upbad") elif evt == "sync": m = None; t = args[0] if t == "login": @@ -137,7 +135,7 @@ def onEvent(self, evt, *args): elif evt == "clockOff": self._clockOff() elif evt == "checkFailed": - self._checkFailed() + self._checkFailed("checkFailed") elif evt == "mediaSanity": showWarning(_("""\ A problem occurred while syncing media. Please use Tools>Check Media, then \ @@ -300,12 +298,13 @@ def _clockOff(self): Syncing requires the clock on your computer to be set correctly. Please \ fix the clock and try again.""")) - def _checkFailed(self): + def _checkFailed(self, event): """State to check database before syncing. (Actually, it's totally useless if the database is downloaded the current will be deleted.)""" showWarning(_("""\ Your collection is in an inconsistent state. Please run Tools>\ -Check Database, then sync again.""")) +Check Database, then sync again. +This is due to event {event} during sync.""")) # Sync thread ###################################################################### diff --git a/designer/preferences.ui b/designer/preferences.ui index 0009a7bf405..14c2d851854 100644 --- a/designer/preferences.ui +++ b/designer/preferences.ui @@ -7,7 +7,7 @@ 0 0 423 - 508 + 552 @@ -20,7 +20,7 @@ Qt::StrongFocus - 0 + 3 @@ -474,7 +474,7 @@ 0 0 - 401 + 445 471 @@ -489,6 +489,16 @@ + + + + Note with no card: create card 1 instead of deleting the note + + + true + + + diff --git a/difference.md b/difference.md index eeaf6f0f9f3..8bd2c73b80e 100644 --- a/difference.md +++ b/difference.md @@ -2,3 +2,14 @@ This files list the difference between regular anki and this forked version. It also lists the different options in the Preferences's extra page. +## Explain errors +You obtain more detailled error message if a sync fail, and if you try +do do a «Check database». + +It transform the very long method `fixIntegrity` into plenty of small +function. It would helps to do add-ons for this forked version of anki. + +In the preferences, the button «Note with no card: create card 1 +instead of deleting the note» chage the behavior of anki when he finds +a note which has no more card. This allow to lose the content of the +note, and let you correct the note instead to generate cards. From c971dad263d302816215da94feb64e55a69a4dcc Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Mon, 17 Jun 2019 11:43:54 +0200 Subject: [PATCH 60/68] =?UTF-8?q?Have=20a=20readable=20=C2=ABempty=20cards?= =?UTF-8?q?=C2=BB=20message?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- addons/25425599/LICENSE | 674 ++++++++++++++++++++++++++++++++++++ addons/25425599/README.md | 40 +++ addons/25425599/__init__.py | 1 + addons/25425599/init.py | 30 ++ addons/25425599/meta.json | 1 + addons/25425599/zipping | 2 + anki/collection.py | 22 +- aqt/addons.py | 1 + difference.md | 3 + 9 files changed, 769 insertions(+), 5 deletions(-) create mode 100644 addons/25425599/LICENSE create mode 100644 addons/25425599/README.md create mode 100644 addons/25425599/__init__.py create mode 100644 addons/25425599/init.py create mode 100644 addons/25425599/meta.json create mode 100644 addons/25425599/zipping diff --git a/addons/25425599/LICENSE b/addons/25425599/LICENSE new file mode 100644 index 00000000000..f288702d2fa --- /dev/null +++ b/addons/25425599/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/addons/25425599/README.md b/addons/25425599/README.md new file mode 100644 index 00000000000..a12c4c99d51 --- /dev/null +++ b/addons/25425599/README.md @@ -0,0 +1,40 @@ +# Field name in card deletion message +## Rationale +Let's say you use the "empty cards" action. It shows a messages of the +form: + +> Empty card numbers: 3,4 +> Fields: [b] / Voiced
Bilabial
Plosive / [sound:e6fa97a9bb605e35c1a6694a19f83ef3.mp3] + +are not really usefull. Because I've no clue what card number 3 +is. Worst, I can't even go to check, because I can't do anything until +anki knows whether I want to delete those cards or not. + +With this add-on, you'll instead get the message: +> Empty card numbers: Example, Test, +> Fields: [b] / Voiced
Bilabial
Plosive / [sound:e6fa97a9bb605e35c1a6694a19f83ef3.mp3] + +So you know that cards Example and Test will be deleted. +## Warning +The deletion process is exactly the same as before, only the message +is changed. So there should be no problem. + +## Internal +It changes method anki._Collection.emptyCardReport and don't call +the last version of this method +## Advice +Maybe think about using [Delete empty new +cards](https://ankiweb.net/shared/info/1402327111), so that you +don't delete a card you already saw by accident + +## Version 2.0 +None +## Links, licence and credits + +Key |Value +------------|------------------------------------------------------------------- +Copyright | Arthur Milchior +Based on | Anki code by Damien Elmes +License | GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +Source in | https://github.com/Arthur-Milchior/anki-clearer-empty-card +Addon number| [25425599](https://ankiweb.net/shared/info/25425599) diff --git a/addons/25425599/__init__.py b/addons/25425599/__init__.py new file mode 100644 index 00000000000..ce1f9a5ad35 --- /dev/null +++ b/addons/25425599/__init__.py @@ -0,0 +1 @@ +from . import init diff --git a/addons/25425599/init.py b/addons/25425599/init.py new file mode 100644 index 00000000000..4bd3de60551 --- /dev/null +++ b/addons/25425599/init.py @@ -0,0 +1,30 @@ +from anki.collection import _Collection +from aqt import mw +from anki.utils import ids2str +from anki.consts import * +from anki.lang import _ + +def emptyCardReport(self, cids): + col = mw.col + models = col.models + rep = "" + for ords, mid, flds in self.db.all(""" + select group_concat(ord), mid, flds from cards c, notes n + where c.nid = n.id and c.id in %s group by nid order by mid""" % ids2str(cids)): + model = models.get(mid) + modelName = model["name"] + templates = model["tmpls"] + isCloze = model["type"] == MODEL_CLOZE + rep += _("Empty cards")+" ("+modelName+"): " + if isCloze: + rep+=ords + else: + for ord in ords.split(","): + ord = int(ord) + templateName = templates[ord]["name"] + rep += templateName+", " + rep +="\nFields: %(f)s\n\n" % dict(f=flds.replace("\x1f", " / ")) + return rep + + +_Collection.emptyCardReport = emptyCardReport diff --git a/addons/25425599/meta.json b/addons/25425599/meta.json new file mode 100644 index 00000000000..85aa581d76e --- /dev/null +++ b/addons/25425599/meta.json @@ -0,0 +1 @@ +{"name": "Empty cards returns more usable informations", "mod": 1560126141} \ No newline at end of file diff --git a/addons/25425599/zipping b/addons/25425599/zipping new file mode 100644 index 00000000000..36602663056 --- /dev/null +++ b/addons/25425599/zipping @@ -0,0 +1,2 @@ +rm betterEmptyCardReport.zip +zip betterEmptyCardReport.zip *py README.md LICENSE config* zipping \ No newline at end of file diff --git a/anki/collection.py b/anki/collection.py index adee4b49c8d..c80e872f2a9 100644 --- a/anki/collection.py +++ b/anki/collection.py @@ -656,12 +656,24 @@ def emptyCids(self): return rem def emptyCardReport(self, cids): + models = self.models rep = "" - for ords, cnt, flds in self.db.all(""" -select group_concat(ord+1), count(), flds from cards c, notes n -where c.nid = n.id and c.id in %s group by nid""" % ids2str(cids)): - rep += _("Empty card numbers: %(c)s\nFields: %(f)s\n\n") % dict( - c=ords, f=flds.replace("\x1f", " / ")) + for ords, mid, flds in self.db.all(""" + select group_concat(ord), mid, flds from cards c, notes n + where c.nid = n.id and c.id in %s group by nid order by mid""" % ids2str(cids)): + model = models.get(mid) + modelName = model["name"] + templates = model["tmpls"] + isCloze = model["type"] == MODEL_CLOZE + rep += _("Empty cards")+" ("+modelName+"): " + if isCloze: + rep+=ords + else: + for ord in ords.split(","): + ord = int(ord) + templateName = templates[ord]["name"] + rep += templateName+", " + rep +="\nFields: %(f)s\n\n" % dict(f=flds.replace("\x1f", " / ")) return rep # Field checksums and sorting fields diff --git a/aqt/addons.py b/aqt/addons.py index 70805b87a28..e292b35602e 100644 --- a/aqt/addons.py +++ b/aqt/addons.py @@ -902,6 +902,7 @@ def __hash__(self): """ Set of characteristic of Add-ons incorporated here""" incorporatedAddonsSet = { Addon("«Check database» Explain errors and what is done to fix it", 1135180054, gitHash = "371c360e5611ad3eec5dcef400d969e7b1572141", gitRepo = "https://github.com/Arthur-Milchior/anki-database-check-explained"), #mod unkwon because it's not directly used by the author anymore + Addon("Empty cards returns more usable informations", 25425599, 1560126141, "299a0a7b3092923f5932da0bf8ec90e16db269af", "https://github.com/Arthur-Milchior/anki-clearer-empty-card"), } incorporatedAddonsDict = {**{addon.name: addon for addon in incorporatedAddonsSet if addon.name}, diff --git a/difference.md b/difference.md index 8bd2c73b80e..98772c7a297 100644 --- a/difference.md +++ b/difference.md @@ -13,3 +13,6 @@ In the preferences, the button «Note with no card: create card 1 instead of deleting the note» chage the behavior of anki when he finds a note which has no more card. This allow to lose the content of the note, and let you correct the note instead to generate cards. + +## Usable card report (25425599) +Add more informations in the «empty card» report. From 9a33d6f5d4e39076d2c98eb3842e6cb5998fbac9 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Thu, 27 Jun 2019 06:21:42 +0200 Subject: [PATCH 61/68] Correct new due --- addons/127334978/LICENSE | 674 +++++++++++++++++++++++++++++++++++ addons/127334978/README.md | 89 +++++ addons/127334978/__init__.py | 1 + addons/127334978/ankiweb | 1 + addons/127334978/redue.py | 27 ++ anki/collection.py | 21 +- aqt/addons.py | 1 + difference.md | 11 + 8 files changed, 819 insertions(+), 6 deletions(-) create mode 100755 addons/127334978/LICENSE create mode 100755 addons/127334978/README.md create mode 100755 addons/127334978/__init__.py create mode 100755 addons/127334978/ankiweb create mode 100755 addons/127334978/redue.py diff --git a/addons/127334978/LICENSE b/addons/127334978/LICENSE new file mode 100755 index 00000000000..f288702d2fa --- /dev/null +++ b/addons/127334978/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/addons/127334978/README.md b/addons/127334978/README.md new file mode 100755 index 00000000000..1f79583bcd8 --- /dev/null +++ b/addons/127334978/README.md @@ -0,0 +1,89 @@ +# Correct new card ordering, clean due number +## Rationale +This add-on solve a problem related to the order of new card. This +problem only appear when the collection is quite big (I had more than +100,000 cards before the problem started to occur), and if you play a +lot with the configuration, trying things. Note that this add-on only +have to be used once, and the bug will be corrected on ankiweb, +ios, ankidroid and any other computer. + +More precisely, anki is supposed to show you all new cards of a note +before showing you cards of another note. There are exceptions to this +rules. For example, when you see a card, you may bury its siblings. In +which case the card which should have been shown today is not shown +and another card must be selected. But the main idea is that, if a +note have a few cards, you'll discover all of those cards near in the +same week, or at worst in the same month. + +However, it may occurs that suddenly, anki decides to show you the +first card of each note before showing you the second card of any +note. Depending on how you use anki, it may be a real problem, and +there is virtually no way to correct it by yourself. This is why this +add-on is here. + +You can see whether you need this add-on by opening a browser, and +searching "due>=1000000". If you see any card it means that this +add-on may help you. Otherwise, this add-on won't hurt, and won't +change anything. + +## Usage +In the main window, select "Tools>clean due". And that's it. + +## Warning +I can't imagine how this add-on could break anything. I hope this +means warning is useless. However, better be safe, and be sure to make +a back up before trying the add-on. + +## Internal +This add-on does not change any method. + +Here is the explanation of the cause of the bug, and of the solution. + +When selecting a new card in a deck `d`, anki selects the card in `d`, +which is new, not suspended, not buried, and whose `due` value is +minimal. This mean that the only purpose of the `due` value of new +card is to decide in which order new cards will be shown. Intuitively, +the `due` value is a computation, done in advance, to find new cards +quickly. Which means that if this computation had an error, new cards +will be found in the wrong order. + +The `due` value of a new note is chosen to be the greatest due value +of the collection, plus one. Which means that the due given to cards +always increase. However, for technical reason, the due is capped at +1,000,000 (this ensures that the due value holds on 32 bits, i.e. on +an int). It is usually not a problem since most collection does not +have a million note. However, if for some reason you have any card +whose `due` value is 1,000,000, all cards will then have this `due` +value. And suddenly `due` does not means anything anymore. + +Thus, this add-on recompute the due value of ALL new cards. Hence, if +you have `n` new cards, the due values will be from 0 to `n`. It will +then randomize the order of new cards when the card is in a deck whose +option requires it. + + +### Possible cause of the problem +The trouble being that, if you change a deck's +option, and choose to see new cards in random order/order of creation, +then anki will give a new due value to each card in decks having this +deck option. + +The bug mentionned above is caused by the fact that the due number of +each card is always incremented and never decremented. However + +The problem may also occur if, in the browser, you select +`Cards>Reposition` and choose a value at least equal to 1,000,000. + +## Version 2.0 +Port by [lovac42](https://github.com/lovac42/anki-correct-due) + +## Links, licence and credits + +Key |Value +------------|------------------------------------------------------------------- +Copyright | Arthur Milchior +Based on | Anki code by Damien Elmes +License | GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +Source in | https://github.com/Arthur-Milchior/anki-correct-due +Addon number| [127334978](https://ankiweb.net/shared/info/127334978) +Support me on| [![Ko-fi](https://ko-fi.com/img/Kofi_Logo_Blue.svg)](Ko-fi.com/arthurmilchior) or [![Patreon](http://www.milchior.fr/patreon.png)](https://www.patreon.com/bePatron?u=146206) diff --git a/addons/127334978/__init__.py b/addons/127334978/__init__.py new file mode 100755 index 00000000000..eb8813a0c6a --- /dev/null +++ b/addons/127334978/__init__.py @@ -0,0 +1 @@ +from . import redue diff --git a/addons/127334978/ankiweb b/addons/127334978/ankiweb new file mode 100755 index 00000000000..49adfb61725 --- /dev/null +++ b/addons/127334978/ankiweb @@ -0,0 +1 @@ +This add-on corrects a bug which only occur in big collections. If new cards appear in the wrong order, you may need to use it. Find more informations on https://github.com/Arthur-Milchior/anki-correct-due to know whether this add-on is really for you. \ No newline at end of file diff --git a/addons/127334978/redue.py b/addons/127334978/redue.py new file mode 100755 index 00000000000..448359954b7 --- /dev/null +++ b/addons/127334978/redue.py @@ -0,0 +1,27 @@ +from aqt.qt import QAction +from aqt import mw + +CARD_NEW = 0 +NEW_CARDS_RANDOM = 0 + +def redue(): + col = mw.col + sched = col.sched + + cids = col.db.list(f"select id from cards order by id and type = {CARD_NEW}") + sched.sortCards(cids) + col.conf['nextPos'] = col.db.scalar( + f"select max(due)+1 from cards where type = {CARD_NEW}") or 0 + + dconfs = col.decks.dconf + + random_dconfs = [dconf for dconf in dconfs.values() if dconf["new"]['order'] == NEW_CARDS_RANDOM] + for dconf in random_dconfs: + sched.resortConf(dconf) + + + +action = QAction(mw) +action.setText("Clean due") +mw.form.menuTools.addAction(action) +action.triggered.connect(redue) diff --git a/anki/collection.py b/anki/collection.py index adee4b49c8d..79670c1fe64 100644 --- a/anki/collection.py +++ b/anki/collection.py @@ -1159,12 +1159,21 @@ def intermediate(self): def atMost1000000Due(self): # new cards can't have a due position > 32 bits - curs = self.db.cursor() - curs.execute(""" - update cards set due = 1000000, mod = ?, usn = ? where due > 1000000 - and type = {CARD_NEW}""", intTime(), self.usn()) - if curs.rowcount: - self.problems.append("Fixed %d cards with due greater than 1000000 due." % curs.rowcount) + cids = self.db.list(f"select id from cards order by id and type = {CARD_NEW} and due > 1000000") + if not cids: + return + + self.sched.sortCards(cids) + col.conf['nextPos'] = col.db.scalar( + f"select max(due)+1 from cards where type = {CARD_NEW}") or 0 + dconfs = col.decks.dconf + + random_dconfs = [dconf for dconf in dconfs.values() if dconf["new"]['order'] == NEW_CARDS_RANDOM] + for dconf in random_dconfs: + sched.resortConf(dconf) + + if cids: + self.problems.append("Fixed %d cards with due greater than 1000000 due." % len(cids)) def setNextPos(self): # new card position diff --git a/aqt/addons.py b/aqt/addons.py index 70805b87a28..387578043a9 100644 --- a/aqt/addons.py +++ b/aqt/addons.py @@ -902,6 +902,7 @@ def __hash__(self): """ Set of characteristic of Add-ons incorporated here""" incorporatedAddonsSet = { Addon("«Check database» Explain errors and what is done to fix it", 1135180054, gitHash = "371c360e5611ad3eec5dcef400d969e7b1572141", gitRepo = "https://github.com/Arthur-Milchior/anki-database-check-explained"), #mod unkwon because it's not directly used by the author anymore + Addon("Correcting a bug in anki which makes new card appearing in wrong order", 127334978, 1561608317, "9ec2f1e5c2f4d95de82b6cc7a43bf68cb39a26f7", "https://github.com/Arthur-Milchior/anki-correct-due"), } incorporatedAddonsDict = {**{addon.name: addon for addon in incorporatedAddonsSet if addon.name}, diff --git a/difference.md b/difference.md index 8bd2c73b80e..1fbbb72d66a 100644 --- a/difference.md +++ b/difference.md @@ -9,6 +9,17 @@ do do a «Check database». It transform the very long method `fixIntegrity` into plenty of small function. It would helps to do add-ons for this forked version of anki. +## Correcting due (127334978) +Anki precomputes the order of the new cards to see. While in theory, +this is all nice, in practice it bugs in some strange case. Those +cases may occur in particular if you download a shared deck having +this bug. If you want details, it is explained here +https://github.com/Arthur-Milchior/anki/blob/master/documentation/due.md + +Otherwise, you don't need to care about this change, except may be if +you use an add-on which itself change the order of card, such as the +«Hoochie»'s add-ons. + In the preferences, the button «Note with no card: create card 1 instead of deleting the note» chage the behavior of anki when he finds a note which has no more card. This allow to lose the content of the From 02c3005d3fc0055a8020bf271b94f82a386c03de Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Mon, 17 Jun 2019 22:00:55 +0200 Subject: [PATCH 62/68] Quicker change model --- addons/802285486/LICENSE | 674 +++++++++++++++++++++++++++++++++ addons/802285486/README.md | 113 ++++++ addons/802285486/__init__.py | 1 + addons/802285486/ankiweb | 5 + addons/802285486/aqtmodels.py | 17 + addons/802285486/clayout.py | 111 ++++++ addons/802285486/collection.py | 220 +++++++++++ addons/802285486/debug.py | 167 ++++++++ addons/802285486/editor.py | 9 + addons/802285486/fields.py | 44 +++ addons/802285486/models.py | 178 +++++++++ addons/802285486/sched.py | 91 +++++ anki/collection.py | 52 +-- anki/models.py | 102 ++++- aqt/addons.py | 1 + aqt/clayout.py | 18 +- aqt/editor.py | 2 +- aqt/fields.py | 6 +- aqt/models.py | 4 +- difference.md | 4 + 20 files changed, 1769 insertions(+), 50 deletions(-) create mode 100755 addons/802285486/LICENSE create mode 100755 addons/802285486/README.md create mode 100755 addons/802285486/__init__.py create mode 100755 addons/802285486/ankiweb create mode 100755 addons/802285486/aqtmodels.py create mode 100755 addons/802285486/clayout.py create mode 100755 addons/802285486/collection.py create mode 100755 addons/802285486/debug.py create mode 100755 addons/802285486/editor.py create mode 100755 addons/802285486/fields.py create mode 100755 addons/802285486/models.py create mode 100755 addons/802285486/sched.py diff --git a/addons/802285486/LICENSE b/addons/802285486/LICENSE new file mode 100755 index 00000000000..f288702d2fa --- /dev/null +++ b/addons/802285486/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/addons/802285486/README.md b/addons/802285486/README.md new file mode 100755 index 00000000000..a2b9325711d --- /dev/null +++ b/addons/802285486/README.md @@ -0,0 +1,113 @@ +# Quicker Anki +Plenty of part of anki may become more efficient. More precisely, anki +is not optimized for power user, and some actions may become slow when +you have a big collection or complicated notes. This add-on improve +the speed of some actions. In the case where you only use basic card +and have a small collection, you probably won't see any difference +while installing this add-on. + +## Warning +This add-on change plenty of methods. Which means that this add-on +risks to be incompatible with other add-on. Incompatible add-on will +be listed here when I learn about them. + +In particular it is incompatible with add-ons: +* [12287769](https://ankiweb.net/shared/info/12287769) «Explain +deletion». This add-on ensure that the file `deleted.txt` state +why notes are deleted. +* [Database checker/fixer explained, more fixers 1135180054](https://ankiweb.net/shared/info/1135180054) + +Please instead use-addon +[777545149](https://ankiweb.net/shared/info/777545149) which merges +those three add-ons + +## What this add-on improve +### Note type with a lot of card type +Assume you have a note type with a lot of card type, or with a lot of +card. +* Assume you edit the note type. Then, closing the editor and + saving the note type may take several minutes. This is because anki + recompute some useless data and does not consider the fact that + some template did not change. +* Assume that you add a card, it sometime takes many seconds. A big + part is also lost recomputing useless data. + +This add-on just ensure that those computations are done only when it +is required. + +### Reordering cards +Let us assume you switch a deck so that cards are selected in random +order (or so that they are selected in creation order). Then anki +reorder every card, and they discard the reordering of cards which are +not new, which makes no sens. + +This add-on ensures that only new cards are reordered. + + +## Usage +Just install this add-on. +## Version 2.0 +None +## Internal +We give methods changed by this add-on. + +Both adding card and changing note type require to change: +* anki.models.ModelManager.save + +To change note type's editino, we change: +* aqt.clayout.CardLayout.__init__ +* aqt.clayout.CardLayout.onRemove +* aqt.clayout.CardLayout.onReorder +* aqt.clayout.CardLayout.onAddCard +* aqt.clayout.CardLayout.reject +* aqt.fields.FieldDialog.reject +* aqt.fields.FieldDialog.__init__ +* aqt.fields.FieldDialog._uniqueName +* anki.collection._Collection.genCards +* anki.models.ModelManager._updateRequired +* anki.models.ModelManager._syncTemplates +* anki.models.ModelManager.availOrds + +To improve the creation of note, we change: +* aqt.editor.Editor.saveAddModeVars +* aqt.fields.FieldDialog.reject +* aqt.models.Moleds.onRename +* aqt.models.Moleds.modelChanged +* aqt.models.Moleds.onAdd +* aqt.models.Moleds.saveModel + +To reorder card quickly, we change: +* anki.sched.Scheduler.randomizeCards +* anki.sched.Scheduler.orderCards +* anki.sched.Scheduler.sortCards +* anki.schedv2.Scheduler.randomizeCards +* anki.schedv2.Scheduler.orderCards +* anki.schedv2.Scheduler.sortCards + + + +## Links, licence and credits + +Key |Value +------------|------------------------------------------------------------------- +Copyright | Arthur Milchior +Based on | Anki code by Damien Elmes +License | GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +Source in | https://github.com/Arthur-Milchior/anki-fast-note-type-editor +Addon number| [802285486](https://ankiweb.net/shared/info/802285486) +Support me on| [![Ko-fi](https://ko-fi.com/img/Kofi_Logo_Blue.svg)](Ko-fi.com/arthurmilchior) or [![Patreon](http://www.milchior.fr/patreon.png)](https://www.patreon.com/bePatron?u=146206) + +## Notes + +I believe it should be implemented in anki's core code, but this was +refused. +* https://anki.tenderapp.com/discussions/ankidesktop/32549-potential-pull-request +* https://github.com/dae/anki/pull/297#issuecomment-481120247 + +Some methods calling `anki.models.ModelManager.save` are not +modified, because they are long and rarely used; so it's better no to +touch them to avoid incompatibility with : +* anki.collection._Collection.fixIntegrity +* aqt.importing.ImportDialog.accept +* anki.storage._upgradeSchema +* anki.storage._upgradeClozeModel diff --git a/addons/802285486/__init__.py b/addons/802285486/__init__.py new file mode 100755 index 00000000000..f0f9acdb45b --- /dev/null +++ b/addons/802285486/__init__.py @@ -0,0 +1 @@ +from . import clayout, collection, editor, fields, models, aqtmodels, sched diff --git a/addons/802285486/ankiweb b/addons/802285486/ankiweb new file mode 100755 index 00000000000..71cae050f73 --- /dev/null +++ b/addons/802285486/ankiweb @@ -0,0 +1,5 @@ +Assume you have a note type with a lot of card type, or with a lot of card. Assume you edite the note type. Then, closing the editor and saving the note type may take several minutes. This is because anki redoes some useless computation and does not consider the fact that some template did not change.n + +This add-on just ensure that this computation is done only when it is required. + +For more information, go to https://github.com/Arthur-Milchior/anki-fast-note-type-editor \ No newline at end of file diff --git a/addons/802285486/aqtmodels.py b/addons/802285486/aqtmodels.py new file mode 100755 index 00000000000..6a524c93030 --- /dev/null +++ b/addons/802285486/aqtmodels.py @@ -0,0 +1,17 @@ +from aqt.models import Models +from aqt.utils import getText +from aqt.models import AddModel + +def onRename(self): + txt = getText(_("New name:"), default=self.model['name']) + if txt[1] and txt[0]: + self.model['name'] = txt[0] + self.mm.save(self.model, recomputeReq=False) + self.updateModelsList() + +Models.onRename = onRename + +def modelChanged(self): + idx = self.form.modelsList.currentRow() + self.model = self.models[idx] +Models.modelChanged = modelChanged diff --git a/addons/802285486/clayout.py b/addons/802285486/clayout.py new file mode 100755 index 00000000000..9a96b14998e --- /dev/null +++ b/addons/802285486/clayout.py @@ -0,0 +1,111 @@ +from aqt.clayout import CardLayout +from aqt.qt import *#for ngettext, QDialog +from anki.lang import _ +from aqt.utils import showWarning, askUser, showInfo, getOnlyText, saveGeom +from anki.sound import clearAudioQueue +import copy +from .debug import debugFun + + +oldInit = CardLayout.__init__ +@debugFun +def init(self,*args,**kwargs): + try: + oldInit(self,*args,**kwargs) + except ValueError: + print (f"recursive model is {self.model}") + raise + # self.oldIdxToNew = list(range(len(self.model['tmpls']))) + self.newTemplatesData = [ + {"is new":False, + "old idx":idx} + for idx in (range(len(self.model['tmpls'])))] + self.originalModel = copy.deepcopy(self.model) + + +CardLayout.__init__ = init +@debugFun +def onRemove(self): + if len(self.model['tmpls']) < 2: + return showInfo(_("At least one card type is required.")) + idx = self.ord + originalIdx = self.newTemplatesData[idx]["old idx"] + cards = self.mm.tmplUseCount(self.model, idx) + cards = ngettext("%d card", "%d cards", cards) % cards + msg = (_("Delete the '%(a)s' card type, and its %(b)s?") % + dict(a=self.model['tmpls'][idx]['name'], b=cards)) + if not askUser(msg): + return + if not self.mm.remTemplate(self.model, self.cards[idx].template()): + return showWarning(_("""\ +Removing this card type would cause one or more notes to be deleted. \ +Please create a new card type first.""")) + + # if originalIdx is not None: + # oldIdxToNew[originalIdx] = None + del self.newTemplatesData[idx] + self.redraw() +CardLayout.onRemove = onRemove + + +@debugFun +def onReorder(self): + n = len(self.cards) + cur = self.ord+1 + pos = getOnlyText( + _("Enter new card position (1...%s):") % n, + default=str(cur)) + idx = self.ord + originalMeta = self.newTemplatesData[idx] + if not pos: + return + try: + pos = int(pos) + except ValueError: + return + if pos < 1 or pos > n: + return + if pos == cur: + return + pos -= 1 + self.mm.moveTemplate(self.model, self.card.template(), pos) + del self.newTemplatesData[idx] + self.newTemplatesData.insert(pos,originalMeta) + self.ord = pos + self.redraw() +CardLayout.onReorder = onReorder + +@debugFun +def onAddCard(self): + cnt = self.mw.col.models.useCount(self.model) + txt = ngettext("This will create %d card. Proceed?", + "This will create %d cards. Proceed?", cnt) % cnt + if not askUser(txt): + return + name = self._newCardName() + t = self.mm.newTemplate(name) + old = self.card.template() + t['qfmt'] = old['qfmt'] + t['afmt'] = old['afmt'] + self.mm.addTemplate(self.model, t) + self.newTemplatesData.append({"old idx":self.newTemplatesData[self.ord]["old idx"], + "is new": True}) + self.ord = len(self.cards) + self.redraw() +CardLayout.onAddCard = onAddCard + +@debugFun +def reject(self):#same as accept + self.cancelPreviewTimer() + clearAudioQueue() + if self.addMode: + # remove the filler fields we added + for name in self.emptyFields: + self.note[name] = "" + self.mw.col.db.execute("delete from notes where id = ?", + self.note.id) + self.mm.save(self.model, templates=True, oldModel = self.originalModel, newTemplatesData = self.newTemplatesData) + self.mw.reset() + saveGeom(self, "CardLayout") + return QDialog.reject(self) +CardLayout.reject = reject diff --git a/addons/802285486/collection.py b/addons/802285486/collection.py new file mode 100755 index 00000000000..3f290449423 --- /dev/null +++ b/addons/802285486/collection.py @@ -0,0 +1,220 @@ +from anki.collection import _Collection +from anki.utils import ids2str, maxID, intTime +from .debug import debugFun +import os +import stat +from anki.consts import * +from anki.lang import _ + +@debugFun +def genCards(self, nids, changedOrNewReq = None): + """Ids of cards needed to be removed. + Generate missing cards of a note with id in nids and with ord in changedOrNewReq. + """ + # build map of (nid,ord) so we don't create dupes + snids = ids2str(nids) + have = {}#Associated to each nid a dictionnary from card's order to card id. + dids = {}#Associate to each nid the only deck id containing its cards. Or None if there are multiple decks + dues = {}#Associate to each nid the due value of the last card seen. + for id, nid, ord, did, due, odue, odid in self.db.execute( + "select id, nid, ord, did, due, odue, odid from cards where nid in "+snids): + # existing cards + if nid not in have: + have[nid] = {} + have[nid][ord] = id + # if in a filtered deck, add new cards to original deck + if odid != 0: + did = odid + # and their dids + if nid in dids: + if dids[nid] and dids[nid] != did: + # cards are in two or more different decks; revert to + # model default + dids[nid] = None + else: + # first card or multiple cards in same deck + dids[nid] = did + # save due + if odid != 0: + due = odue + if nid not in dues: + dues[nid] = due + # build cards for each note + data = []#Tuples for cards to create. Each tuple is newCid, nid, did, ord, now, usn, due + ts = maxID(self.db) + now = intTime() + rem = []#cards to remove + usn = self.usn() + for nid, mid, flds in self.db.execute( + "select id, mid, flds from notes where id in "+snids): + model = self.models.get(mid) + avail = self.models.availOrds(model, flds, changedOrNewReq) + did = dids.get(nid) or model['did'] + due = dues.get(nid) + # add any missing cards + for t in self._tmplsFromOrds(model, avail): + doHave = nid in have and t['ord'] in have[nid] + if not doHave: + # check deck is not a cram deck + did = t['did'] or did + if self.decks.isDyn(did): + did = 1 + # if the deck doesn't exist, use default instead + did = self.decks.get(did)['id'] + # use sibling due# if there is one, else use a new id + if due is None: + due = self.nextID("pos") + data.append((ts, nid, did, t['ord'], + now, usn, due)) + ts += 1 + # note any cards that need removing + if nid in have: + for ord, id in list(have[nid].items()): + if ((changedOrNewReq is None or ord in changedOrNewReq) and + ord not in avail): + rem.append(id) + # bulk update + self.db.executemany(""" +insert into cards values (?,?,?,?,?,?,0,0,?,0,0,0,0,0,0,0,0,"")""", + data) + return rem + +_Collection.genCards = genCards +print("gen cards is changed") + + +## This one is changed only to ask not to recompute models.save +def fixIntegrity(self): + "Fix possible problems and rebuild caches." + problems = [] + self.save() + oldSize = os.stat(self.path)[stat.ST_SIZE] + if self.db.scalar("pragma integrity_check") != "ok": + return (_("Collection is corrupt. Please see the manual."), False) + # note types with a missing model + ids = self.db.list(""" +select id from notes where mid not in """ + ids2str(self.models.ids())) + if ids: + problems.append( + ngettext("Deleted %d note with missing note type.", + "Deleted %d notes with missing note type.", len(ids)) + % len(ids)) + self.remNotes(ids) + # for each model + for m in self.models.all(): + for t in m['tmpls']: + if t['did'] == "None": + t['did'] = None + problems.append(_("Fixed AnkiDroid deck override bug.")) + self.models.save(m, recomputeReq=False) + if m['type'] == MODEL_STD: + # model with missing req specification + if 'req' not in m: + self.models._updateRequired(m) + problems.append(_("Fixed note type: %s") % m['name']) + # cards with invalid ordinal + ids = self.db.list(""" +select id from cards where ord not in %s and nid in ( +select id from notes where mid = ?)""" % + ids2str([t['ord'] for t in m['tmpls']]), + m['id']) + if ids: + problems.append( + ngettext("Deleted %d card with missing template.", + "Deleted %d cards with missing template.", + len(ids)) % len(ids)) + self.remCards(ids) + # notes with invalid field count + ids = [] + for id, flds in self.db.execute( + "select id, flds from notes where mid = ?", m['id']): + if (flds.count("\x1f") + 1) != len(m['flds']): + ids.append(id) + if ids: + problems.append( + ngettext("Deleted %d note with wrong field count.", + "Deleted %d notes with wrong field count.", + len(ids)) % len(ids)) + self.remNotes(ids) + # delete any notes with missing cards + ids = self.db.list(""" +select id from notes where id not in (select distinct nid from cards)""") + if ids: + cnt = len(ids) + problems.append( + ngettext("Deleted %d note with no cards.", + "Deleted %d notes with no cards.", cnt) % cnt) + self._remNotes(ids) + # cards with missing notes + ids = self.db.list(""" +select id from cards where nid not in (select id from notes)""") + if ids: + cnt = len(ids) + problems.append( + ngettext("Deleted %d card with missing note.", + "Deleted %d cards with missing note.", cnt) % cnt) + self.remCards(ids) + # cards with odue set when it shouldn't be + ids = self.db.list(""" +select id from cards where odue > 0 and (type=1 or queue=2) and not odid""") + if ids: + cnt = len(ids) + problems.append( + ngettext("Fixed %d card with invalid properties.", + "Fixed %d cards with invalid properties.", cnt) % cnt) + self.db.execute("update cards set odue=0 where id in "+ + ids2str(ids)) + # cards with odid set when not in a dyn deck + dids = [id for id in self.decks.allIds() if not self.decks.isDyn(id)] + ids = self.db.list(""" +select id from cards where odid > 0 and did in %s""" % ids2str(dids)) + if ids: + cnt = len(ids) + problems.append( + ngettext("Fixed %d card with invalid properties.", + "Fixed %d cards with invalid properties.", cnt) % cnt) + self.db.execute("update cards set odid=0, odue=0 where id in "+ + ids2str(ids)) + # tags + self.tags.registerNotes() + # field cache + for m in self.models.all(): + self.updateFieldCache(self.models.nids(m)) + # new cards can't have a due position > 32 bits + self.db.execute(""" +update cards set due = 1000000, mod = ?, usn = ? where due > 1000000 +and type = 0""", intTime(), self.usn()) + # new card position + self.conf['nextPos'] = self.db.scalar( + "select max(due)+1 from cards where type = 0") or 0 + # reviews should have a reasonable due # + ids = self.db.list( + "select id from cards where queue = 2 and due > 100000") + if ids: + problems.append("Reviews had incorrect due date.") + self.db.execute( + "update cards set due = ?, ivl = 1, mod = ?, usn = ? where id in %s" + % ids2str(ids), self.sched.today, intTime(), self.usn()) + # v2 sched had a bug that could create decimal intervals + curs = self.db.cursor() + + curs.execute("update cards set ivl=round(ivl),due=round(due) where ivl!=round(ivl) or due!=round(due)") + if curs.rowcount: + problems.append("Fixed %d cards with v2 scheduler bug." % curs.rowcount) + + curs.execute("update revlog set ivl=round(ivl),lastIvl=round(lastIvl) where ivl!=round(ivl) or lastIvl!=round(lastIvl)") + if curs.rowcount: + problems.append("Fixed %d review history entries with v2 scheduler bug." % curs.rowcount) + # and finally, optimize + self.optimize() + newSize = os.stat(self.path)[stat.ST_SIZE] + txt = _("Database rebuilt and optimized.") + ok = not problems + problems.append(txt) + # if any problems were found, force a full sync + if not ok: + self.modSchema(check=False) + self.save() + return ("\n".join(problems), ok) + +_Collection.fixIntegrity = fixIntegrity diff --git a/addons/802285486/debug.py b/addons/802285486/debug.py new file mode 100755 index 00000000000..370ec46fa8c --- /dev/null +++ b/addons/802285486/debug.py @@ -0,0 +1,167 @@ +import re +from inspect import stack + +#whether debug may be turned on eventually. Less efficient +mayDebug = False + +#Whether right debuging is on +shouldDebug = True +def startDebug(): + global shouldDebug + shouldDebug = True + print("Debug started") +def endDebug(): + global shouldDebug + shouldDebug = False + print("Debug ended") + +indentation = 0 +def debug(text, indentToAdd=0, force = False, level =1): + if not shouldDebug and not force: + return + global indentation + glob = stack()[level].frame.f_globals + loc = stack()[level].frame.f_locals + text = eval(f"""f"{text}" """,glob,loc) + indentToPrint = indentation + t = " "*indentToPrint + if indentToAdd>0: + t+= "{<" + space = " " + newline = "\n" + t+= re.sub(newline,newline+space,text) + print (t) + indentation +=indentToAdd + if indentToAdd<0: + indentToPrint +=indentToAdd + print((" "*indentToPrint)+">}") + +nbInsideThis = 0 +def debugInsideThisMethod(fun): + if not mayDebug: + return fun + def aux_debugInsideThisMethod(*args, **kwargs): + global nbInsideThis + startDebug() + nbInsideThis+=1 + ret = fun(*args, **kwargs) + nbInsideThis -=1 + if nbInsideThis ==0: + endDebug() + return ret + return aux_debugInsideThisMethod + +def debugOnlyThisMethod(fun): + return debugFun(fun, (lambda text,indentToAdd = 0: debug(text, indentToAdd, force = True, level=2))) + +def assertEqual(left, right): + if left == right: + return True + print(f"""\n\nReceived\n\"\"\"{left}\"\"\"\nwhich is distinct from expected\n\"\"\"{right}\"\"\"\n""") + if hasattr(left,"firstDifference"): + if hasattr(right,"firstDifference"): + pair = left.firstDifference(right) + if isinstance(pair,tuple): + left_dif,right_dif=pair + print(f"""\n\nThe first difference is\n\"\"\"{left_dif}\"\"\"\nand\n\"\"\"{right_dif}\"\"\"\n""") + elif isinstance(pair,None): + print("Strangely, firstDifference find no difference") + else: + assert False + else: + print("Only the first is a Gen") + elif hasattr(right,"firstDifference"): + print("Only the second is a Gen") + return False + +# def assertEqualString(left, right): +# glob = stack()[1].frame.f_globals +# loc = stack()[1].frame.f_locals +# # try: +# leftEval = eval(left, glob, loc) +# # except NameError as n: +# # print(f"""glob is {glob}""") +# # raise +# rightEval = eval(right,glob,loc) +# if leftEval == rightEval: +# return True +# print(f"""\n\n{left} evaluates as \n"{leftEval}".\n"{rightEval}"\n is the value of {right}, they are distinct.""") +# return False + +def assertType(element,types): + if not isinstance(types,list): + types = [types] + for typ in types: + if isinstance(element,typ): + return True + print(f""" "{element}"'s type is {type(element)}, which is not a subtype of {types}""") + return False + + +def debugFun(fun, debug=debug): + if not mayDebug: + return fun + def aux_debugFun(*args, **kwargs): + nonlocal debug + t = f"{fun.__qualname__}(" + first = False + def comma(text): + nonlocal first, t + if not first: + first = True + else: + t+=", " + t+=text + for arg in args: + comma(f"{arg}") + for kw in kwargs: + comma(f"{kw}={kwargs[kw]}") + t+=")" + debug("{t}",1) + ret = fun(*args, **kwargs) + debug("returns {ret}",-1) + return ret + aux_debugFun.__name__ = f"debug_{fun.__name__}" + aux_debugFun.__qualname__ = f"debug_{fun.__qualname__}" + return aux_debugFun + + +def debugInit(fun, debug=debug): + if not mayDebug: + return fun + def aux_debugInit(self, *args, **kwargs): + t = f"{fun.__name__}(" + needSeparator = False + def comma(text): + nonlocal needSeparator, t + if not needSeparator: + needSeparator = True + else: + t+=", " + t+=text + isSelf = True + for arg in args: + if isSelf: + isSelf = False + continue + comma(f"{arg}") + for kw in kwargs: + comma(f"{kw}={kwargs[kw]}") + t+=")" + debug("{t}",1) + fun(self, *args, **kwargs) + debug("returns {self}",-1) + aux_debugInit.__name__ = f"debug_{fun.__name__}" + aux_debugInit.__qualname__ = f"debug_{fun.__qualname__}" + return aux_debugInit + +def debugOnlyThisInit(fun): + return debugInit(fun, (lambda text,indentToAdd = 0: debug(text, indentToAdd, force = True, level=2))) + + +class ExceptionInverse(Exception): + def __init__(self,text): + self.text = "\n".join(reversed((str(text)+"\n").split("\n"))) + + def __str__(self): + return f"Exception: {self.text}" diff --git a/addons/802285486/editor.py b/addons/802285486/editor.py new file mode 100755 index 00000000000..dd64923c1c0 --- /dev/null +++ b/addons/802285486/editor.py @@ -0,0 +1,9 @@ +from aqt.editor import Editor + +def saveAddModeVars(self): + if self.addMode: + # save tags to model + m = self.note.model() + m['tags'] = self.note.tags + self.mw.col.models.save(m, recomputeReq=False) +Editor.saveAddModeVars = saveAddModeVars diff --git a/addons/802285486/fields.py b/addons/802285486/fields.py new file mode 100755 index 00000000000..1b4b47ec54f --- /dev/null +++ b/addons/802285486/fields.py @@ -0,0 +1,44 @@ +from aqt.fields import FieldDialog +from aqt.utils import getOnlyText, showWarning +from aqt.qt import *#for QDialog +import copy + +def _uniqueName(self, prompt, ignoreOrd=None, old=""): + """Ask for a new name using prompt. Return it. + + Unles this name is already used elsewhere, in this case, return None and show a warning. + If default name is given, return None.""" + txt = getOnlyText(prompt, default=old) + if not txt: + return + for f in self.model['flds']: + if ignoreOrd is not None and f['ord'] == ignoreOrd: + if f['name'] == txt: + return + continue + if f['name'] == txt: + showWarning(_("That field name is already used.")) + return + return txt +FieldDialog._uniqueName = _uniqueName + +oldInit = FieldDialog.__init__ +def init(self, mw, note, *args,**kwargs): + self.originalModel = copy.deepcopy(note.model()) + oldInit(self, mw, note, *args,**kwargs) + +FieldDialog.__init__ = init +print("Changing field dialog") + +#TODO: Recompute template having a field changed. +# def reject(self): +# print("Calling field's new reject") +# self.saveField() +# if self.oldSortField != self.model['sortf']: +# self.mw.progress.start() +# self.mw.col.updateFieldCache(self.mm.nids(self.model)) +# self.mw.progress.finish() +# self.mm.save(self.model, templates = True, oldModel = self.originalModel, recomputeReq = True) +# self.mw.reset() +# QDialog.reject(self) +# FieldDialog.reject = reject diff --git a/addons/802285486/models.py b/addons/802285486/models.py new file mode 100755 index 00000000000..3b6c5f560e9 --- /dev/null +++ b/addons/802285486/models.py @@ -0,0 +1,178 @@ +from anki.models import ModelManager +from anki.utils import intTime, splitFields +from anki.consts import * +from anki.hooks import runHook +from .debug import debugFun +import re + +@debugFun +def getChangedTemplates(m, oldModel = None, newTemplatesData=None): + if newTemplatesData is None: + changedTemplates = set(range(len(m['tmpls']))) + return changedTemplates + changedTemplates = set() + for idx, tmpl in enumerate(m['tmpls']): + oldIdx = newTemplatesData[idx]["old idx"] + if oldIdx is None: + changedTemplates.add(idx) + else: + oldTmpl =oldModel['tmpls'][oldIdx] + if tmpl['qfmt']!=oldTmpl['qfmt']: + changedTemplates.add(idx) + return changedTemplates + +@debugFun +def save(self, m=None, templates=False, oldModel=None, newTemplatesData = None, recomputeReq=True): + """ + * Mark m modified if provided. + * Schedule registry flush. + * Calls hook newModel + Keyword arguments: + m -- A Model + templates -- whether to check for cards not generated in this model + oldModel -- a previous version of the model, to which to compare + newTemplatesData -- a list whose i-th element state which is the + new position of the i-th template of the old model and whether the + template is new. It is set only if oldModel is set. + """ + # print(f"""oldModel is: « +# {oldModel} +# », newTemplatesData is « +# {newTemplatesData} +# »""") + if m and m['id']: + if newTemplatesData is None: + newTemplatesData = [{"is new": True, + "old idx":None}]*len(m['tmpls']) + m['mod'] = intTime() + m['usn'] = self.col.usn() + if recomputeReq: + changedOrNewReq = self._updateRequired(m, oldModel, newTemplatesData) + else: + changedOrNewReq = set() + if templates: + self._syncTemplates(m, changedOrNewReq) + self.changed = True + runHook("newModel") + #print(f"""After saving, model is « +# {m} +# »""") + +ModelManager.save = save +@debugFun +def _updateRequired(self, m, oldModel = None, newTemplatesData = None): + """Entirely recompute the model's req value. + + Return positions idx such that the req for idx in model is not the + req for oldIdx in oldModel. Or such that this card is new. + """ + if m['type'] == MODEL_CLOZE: + # nothing to do + return + changedTemplates = getChangedTemplates(m, oldModel, newTemplatesData) + req = [] + changedOrNewReq = set() + flds = [f['name'] for f in m['flds']] + for idx,t in enumerate(m['tmpls']): + oldIdx = newTemplatesData[idx]["old idx"]# Assumed not None, + oldTup = oldModel['req'][oldIdx] if oldIdx is not None and oldModel else None + if oldModel is not None and idx not in changedTemplates : + if oldTup is None: + print(f"newTemplatesData is «{newTemplatesData}»") + print(f"oldIdx is «{oldIdx}»") + print(f"oldReq is «oldModel['req']»") + assert False + oldIdx, oldType, oldReq_ = oldTup + tup = (idx, oldType, oldReq_) + req.append(tup) + if newTemplatesData[idx]["is new"]: + changedOrNewReq.add(idx) + continue + else: + ret = self._reqForTemplate(m, flds, t) + tup = (idx, ret[0], ret[1]) + if oldTup is None or oldTup[1]!=tup[1] or oldTup[2]!=tup[2]: + changedOrNewReq.add(idx) + req.append(tup) + m['req'] = req + return changedOrNewReq +ModelManager._updateRequired = _updateRequired + +@debugFun +def _syncTemplates(self, m, changedOrNewReq = None): + """Generate all cards not yet generated from, whose note's model is m""" + rem = self.col.genCards(self.nids(m), changedOrNewReq) +ModelManager._syncTemplates = _syncTemplates + +@debugFun +def availOrds(self, m, flds, changedOrNewReq = None): + #oldModel = None, newTemplatesData = None + """Given a joined field string, return template ordinals which should be + seen. See ../documentation/templates_generation_rules.md for + the detail + """ + if m['type'] == MODEL_CLOZE: + return self._availClozeOrds(m, flds) + fields = {} + for c, f in enumerate(splitFields(flds)): + fields[c] = f.strip() + avail = []#List of ord cards which would be generated + for tup in m['req']: + # print(f"""tup is {tup}. + # m['req'] is {m['req']} + # m is {m}""") + ord, type, req = tup + if changedOrNewReq is not None and ord not in changedOrNewReq: + continue + # unsatisfiable template + if type == "none": + continue + # AND requirement? + elif type == "all": + ok = True + for idx in req: + if not fields[idx]: + # missing and was required + ok = False + break + if not ok: + continue + # OR requirement? + elif type == "any": + ok = False + for idx in req: + if fields[idx]: + ok = True + break + if not ok: + continue + avail.append(ord) + return avail + +ModelManager.availOrds = availOrds + +def renameField(self, m, field, newName): + """Rename the field. In each template, find the mustache related to + this field and change them. + m -- the model dictionnary + field -- the field dictionnary + newName -- either a name. Or None if the field is deleted. + """ + self.col.modSchema(check=True) + #Regexp associating to a mustache the name of its field + pat = r'{{([^{}]*)([:#^/]|[^:#/^}][^:}]*?:|)%s}}' + def wrap(txt): + def repl(match): + return '{{' + match.group(1) + match.group(2) + txt + '}}' + return repl + for t in m['tmpls']: + for fmt in ('qfmt', 'afmt'): + if newName: + t[fmt] = re.sub( + pat % re.escape(field['name']), wrap(newName), t[fmt]) + else: + t[fmt] = re.sub( + pat % re.escape(field['name']), "", t[fmt]) + field['name'] = newName + #self.save(m, oldModel = m, newTemplatesData = list(range(len(m['tmpls']))) +ModelManager.renameField = renameField diff --git a/addons/802285486/sched.py b/addons/802285486/sched.py new file mode 100755 index 00000000000..3535b812cf9 --- /dev/null +++ b/addons/802285486/sched.py @@ -0,0 +1,91 @@ +from anki.sched import Scheduler +from anki.schedv2 import Scheduler as SchedV2 +from anki.utils import ids2str, intTime +import random + +CARD_NEW = 0 + +def randomizeCards(self, did): + """Change the due value of new cards of deck `did`. The new due value + is the same for every card of a note (as long as they are in the same + deck.) This number is random.""" + cids = self.col.db.list(f"select id from cards where did = ? and type = {CARD_NEW}", did) + self.sortCards(cids, shuffle=True) + +def orderCards(self, did): + """Change the due value of new cards of deck `did`. The new due value + is the same for every card of a note (as long as they are in + the same deck.) + + The note are now ordered according to the smallest id of their + cards. It generally means they are ordered according to date + creation. + + """ + cids = self.col.db.list(f"select id from cards where did = ? and type = {CARD_NEW} order by id", did) + self.sortCards(cids) + +def sortCards(self, cids, start=1, step=1, shuffle=False, shift=False): + """Change the due of new cards in `cids`. + + Each card of the same note have the same due. The order of the + due is random if shuffle. Otherwise the order of the note `n` is + similar to the order of the first occurrence of a card of `n` in cids. + + Keyword arguments: + cids -- list of card ids to reorder (i.e. change due). Not new cards are ignored + start -- the first due to use + step -- the difference between to successive due of notes + shuffle -- whether to shuffle the note. By default, the order is similar to the created order + shift -- whether to change the due of all new cards whose due is greater than start (to ensure that the new due of cards in cids is not already used) + + """ + scids = ids2str(cids) + now = intTime() + nids = [] + nidsSet = set() + for id in cids: + nid = int(self.col.db.scalar("select nid from cards where id = ?", id)) + if nid not in nidsSet: + nids.append(nid) + nidsSet.add(nid) + if not nids: + # no new cards + return + # determine nid ordering + due = {} + if shuffle: + random.shuffle(nids) + else:#This is new. It ensures that due is sorted according to + #note creation and not card creation. + nids.sort() + for c, nid in enumerate(nids): + due[nid] = start+c*step + # pylint: disable=undefined-loop-variable + high = start+c*step #Highest due which will be used + # shift? + if shift: + low = self.col.db.scalar( + f"select min(due) from cards where due >= ? and type = {CARD_NEW} " + "and id not in %s" % (scids), + start) + if low is not None: + shiftby = high - low + 1 + self.col.db.execute(f""" +update cards set mod=?, usn=?, due=due+? where id not in %s +and due >= ? and queue = {QUEUE_NEW_CRAM}""" % (scids), now, self.col.usn(), shiftby, low) + # reorder cards + d = [] + for id, nid in self.col.db.execute( + (f"select id, nid from cards where type = {CARD_NEW} and id in ")+scids): + d.append(dict(now=now, due=due[nid], usn=self.col.usn(), cid=id)) + self.col.db.executemany( + "update cards set due=:due,mod=:now,usn=:usn where id = :cid", d) + + +Scheduler.randomizeCards = randomizeCards +Scheduler.orderCards = orderCards +Scheduler.sortCards = sortCards +SchedV2.randomizeCards = randomizeCards +SchedV2.orderCards = orderCards +SchedV2.sortCards = sortCards diff --git a/anki/collection.py b/anki/collection.py index adee4b49c8d..691686eb4c9 100644 --- a/anki/collection.py +++ b/anki/collection.py @@ -468,10 +468,10 @@ def _tmplsFromOrds(self, model, avail): ok.append(t) return ok - def genCards(self, nids): + def genCards(self, nids, changedOrNewReq = None): """Ids of cards which needs to be removed. - Generate missing cards of a note with id in nids. + Generate missing cards of a note with id in nids and with ord in changedOrNewReq. """ # build map of (nid,ord) so we don't create dupes snids = ids2str(nids) @@ -510,7 +510,7 @@ def genCards(self, nids): for nid, mid, flds in self.db.execute( "select id, mid, flds from notes where id in "+snids): model = self.models.get(mid) - avail = self.models.availOrds(model, flds) + avail = self.models.availOrds(model, flds, changedOrNewReq) did = dids.get(nid) or model['did'] due = dues.get(nid) # add any missing cards @@ -531,9 +531,12 @@ def genCards(self, nids): ts += 1 # note any cards that need removing if nid in have: - for ord, id in list(have[nid].items()): - if ord not in avail: - rem.append(id) + for ord, cid in list(have[nid].items()): + if ( + (changedOrNewReq is None or ord in changedOrNewReq) + and ord not in avail + ): + rem.append(cid) # bulk update self.db.executemany(""" insert into cards values (?,?,?,?,?,?,0,0,?,0,0,0,0,0,0,0,0,"")""", @@ -1050,7 +1053,7 @@ def fixOverride(self): if t['did'] == "None": t['did'] = None self.problems.append(_("Fixed AnkiDroid deck override bug. (I.e. some template has did = to 'None', it is now None.)")) - self.models.save(m) + self.models.save(m, recomputeReq=False) def fixReq(self): for m in self.models.all(): @@ -1078,12 +1081,12 @@ def fixWrongNumberOfField(self): l = self.db.all( "select id, flds from notes where mid = ?", m['id']) ids = [] - for (id, flds) in l: + for (nid, flds) in l: nbFieldNote = flds.count("\x1f") + 1 nbFieldModel = len(m['flds']) if nbFieldNote != nbFieldModel: - ids.append(id) - self.problems.append(f"""Note {id} with fields «{flds}» has {nbFieldNote} fields while its model {m['name']} has {nbFieldModel} fields""") + ids.append(nid) + self.problems.append(f"""Note {nid} with fields «{flds}» has {nbFieldNote} fields while its model {m['name']} has {nbFieldModel} fields""") if ids: self.remNotes([line[0]for line in lines],reason=f"It hads a wrong number of fields") @@ -1142,7 +1145,7 @@ def fixOdueQueue2(self): def fixOdidOdue(self): self.template( - """select id, odid, did from cards where odid > 0 and did in %s""" % ids2str([id for id in self.decks.allIds() if not self.decks.isDyn(id)]),# cards with odid set when not in a dyn deck + """select id, odid, did from cards where odid > 0 and did in %s""" % ids2str([did for did in self.decks.allIds() if not self.decks.isDyn(did)]),# cards with odid set when not in a dyn deck "Card {}: Set odid and odue to 0 because odid was {} while its did was {} which is not filtered(a.k.a. not dymanic).", "Fixed %d card with invalid properties.", "Fixed %d cards with invalid properties.", @@ -1159,12 +1162,13 @@ def intermediate(self): def atMost1000000Due(self): # new cards can't have a due position > 32 bits - curs = self.db.cursor() - curs.execute(""" - update cards set due = 1000000, mod = ?, usn = ? where due > 1000000 - and type = {CARD_NEW}""", intTime(), self.usn()) - if curs.rowcount: - self.problems.append("Fixed %d cards with due greater than 1000000 due." % curs.rowcount) + curs = self.db.cursor() + curs.execute(""" + update cards set due=1000000+due%1000000,mod=?,usn=? where due>=1000000 + and type=0""", [intTime(), self.usn()]) + if curs.rowcount: + self.problems.append("Found %d new cards with a due number >= 1,000,000 - consider repositioning them in the Browse screen." % curs.rowcount) + def setNextPos(self): # new card position @@ -1205,14 +1209,14 @@ def fixFloatDue(self): def doubleCard(self): l = self.db.all("""select nid, ord, count(*), GROUP_CONCAT(id) from cards group by ord, nid having count(*)>1""") toRemove = [] - for nid, ord, count, ids in l: - ids = ids.split(",") - ids = [int(id) for id in ids] - cards = [Card(self,id) for id in ids] + for nid, ord, count, cids in l: + cids = cids.split(",") + cids = [int(cid) for cid in cids] + cards = [Card(self,cid) for cid in cids] bestCard = max(cards, key = (lambda card: (card.ivl, card.factor, card.due))) - bestId = bestCard.id - self.problems.append(f"There are {count} card for note {nid} at ord {ord}. They are {ids}. We only keep {bestId}") - toRemove += [id for id in ids if id!= bestId] + bestCid = bestCard.id + self.problems.append(f"There are {count} card for note {nid} at ord {ord}. They are {cids}. We only keep {bestCid}") + toRemove += [cid for cid in cids if cid!= bestCid] if toRemove: self.remCards(toRemove) diff --git a/anki/models.py b/anki/models.py index a20cc66ecbe..f2194ac3d96 100644 --- a/anki/models.py +++ b/anki/models.py @@ -142,7 +142,31 @@ def load(self, json_): self.changed = False self.models = json.loads(json_) - def save(self, m=None, templates=False): + def getChangedTemplates(self, m, oldModel = None, newTemplatesData=None): + """A set of index of templates potentially modified. + + keyword arguments: + oldModel -- the previous version of the model. If it is not + present, everything is potentially changed. + newTemplatesData + m -- the new model. + newTemplatesData -- a dictionnarry associating to each new template a dictionnary, with "old idx" representing the old positiof of the card. + """ + if newTemplatesData is None or oldModel is None: + changedTemplates = set(range(len(m['tmpls']))) + return changedTemplates + changedTemplates = set() + for idx, tmpl in enumerate(m['tmpls']): + oldIdx = newTemplatesData[idx]["old idx"] + if oldIdx is None: + changedTemplates.add(idx) + else: + oldTmpl =oldModel['tmpls'][oldIdx] + if tmpl['qfmt']!=oldTmpl['qfmt']: + changedTemplates.add(idx) + return changedTemplates + + def save(self, m=None, templates=False, oldModel=None, newTemplatesData = None, recomputeReq=True): """ * Mark m modified if provided. * Schedule registry flush. @@ -151,13 +175,23 @@ def save(self, m=None, templates=False): Keyword arguments: m -- A Model templates -- whether to check for cards not generated in this model + oldModel -- a previous version of the model, to which to compare + newTemplatesData -- a list whose i-th element state which is the + new position of the i-th template of the old model and whether the + template is new. It is set only if oldModel is set. """ if m and m['id']: + if newTemplatesData is None: + newTemplatesData = [{"is new": True, + "old idx":None}]*len(m['tmpls']) m['mod'] = intTime() m['usn'] = self.col.usn() - self._updateRequired(m) + if recomputeReq: + changedOrNewReq = self._updateRequired(m, oldModel, newTemplatesData) + else: + changedOrNewReq = set() if templates: - self._syncTemplates(m) + self._syncTemplates(m, changedOrNewReq) self.changed = True runHook("newModel") # By default, only refresh side bar of browser @@ -473,7 +507,6 @@ def repl(match): t[fmt] = re.sub( pat % re.escape(field['name']), "", t[fmt]) field['name'] = newName - self.save(m) def _updateFieldOrds(self, m): """ @@ -591,11 +624,11 @@ def moveTemplate(self, m, template, idx): select id from notes where mid = ?)""" % " ".join(map), self.col.usn(), intTime(), m['id']) - def _syncTemplates(self, m): + def _syncTemplates(self, m, changedOrNewReq = None): """Generate all cards not yet generated, whose note's model is m. It's called only when model is saved, a new model is given and template is asked to be computed""" - rem = self.col.genCards(self.nids(m)) + rem = self.col.genCards(self.nids(m), changedOrNewReq) # Model changing ########################################################################## @@ -707,17 +740,42 @@ def scmhash(self, m): # Required field/text cache ########################################################################## - def _updateRequired(self, m): - """Entirely recompute the model's req value""" + def _updateRequired(self, m, oldModel = None, newTemplatesData = None): + """Entirely recompute the model's req value. + + Return positions idx such that the req for idx in model is not the + req for oldIdx in oldModel. Or such that this card is new. + """ if m['type'] == MODEL_CLOZE: # nothing to do return + changedTemplates = self.getChangedTemplates(m, oldModel, newTemplatesData) req = [] + changedOrNewReq = set() flds = [f['name'] for f in m['flds']] - for t in m['tmpls']: - ret = self._reqForTemplate(m, flds, t) - req.append((t['ord'], ret[0], ret[1])) + for idx,t in enumerate(m['tmpls']): + oldIdx = newTemplatesData[idx]["old idx"]# Assumed not None, + oldTup = oldModel['req'][oldIdx] if oldIdx is not None and oldModel else None + if oldModel is not None and idx not in changedTemplates : + if oldTup is None: + print(f"newTemplatesData is «{newTemplatesData}»") + print(f"oldIdx is «{oldIdx}»") + print(f"oldReq is «oldModel['req']»") + assert False + oldIdx, oldType, oldReq_ = oldTup + tup = (idx, oldType, oldReq_) + req.append(tup) + if newTemplatesData[idx]["is new"]: + changedOrNewReq.add(idx) + continue + else: + ret = self._reqForTemplate(m, flds, t) + tup = (idx, ret[0], ret[1]) + if oldTup is None or oldTup[1]!=tup[1] or oldTup[2]!=tup[2]: + changedOrNewReq.add(idx) + req.append(tup) m['req'] = req + return changedOrNewReq def _reqForTemplate(self, m, flds, t): """A rule which is supposed to determine whether a card should be @@ -765,19 +823,25 @@ def _reqForTemplate(self, m, flds, t): req.append(i) return type, req - def availOrds(self, m, flds): - """Given a joined field string, return ordinal of card type which - should be generated. See - ../documentation/templates_generation_rules.md for the detail - - """ + def availOrds(self, m, flds, changedOrNewReq = None): + #oldModel = None, newTemplatesData = None + """Given a joined field string, return template ordinals which should be + seen. See ../documentation/templates_generation_rules.md for + the detail + """ if m['type'] == MODEL_CLOZE: return self._availClozeOrds(m, flds) fields = {} for c, f in enumerate(splitFields(flds)): fields[c] = f.strip() - avail = []#List of ord of cards which would be generated - for ord, type, req in m['req']: + avail = []#List of ord cards which would be generated + for tup in m['req']: + # print(f"""tup is {tup}. + # m['req'] is {m['req']} + # m is {m}""") + ord, type, req = tup + if changedOrNewReq is not None and ord not in changedOrNewReq: + continue # unsatisfiable template if type == "none": continue diff --git a/aqt/addons.py b/aqt/addons.py index 70805b87a28..3d76dacec77 100644 --- a/aqt/addons.py +++ b/aqt/addons.py @@ -901,6 +901,7 @@ def __hash__(self): """ Set of characteristic of Add-ons incorporated here""" incorporatedAddonsSet = { + Addon("Adding note and changing note type become quicker", 802285486, gitHash = "f1b2df03f4040e7820454052a2088a7672d819b2", gitRepo = "https://github.com/Arthur-Milchior/anki-fast-note-type-editor"), Addon("«Check database» Explain errors and what is done to fix it", 1135180054, gitHash = "371c360e5611ad3eec5dcef400d969e7b1572141", gitRepo = "https://github.com/Arthur-Milchior/anki-database-check-explained"), #mod unkwon because it's not directly used by the author anymore } diff --git a/aqt/clayout.py b/aqt/clayout.py index 84cd5da14b4..427b01dbad2 100644 --- a/aqt/clayout.py +++ b/aqt/clayout.py @@ -7,6 +7,7 @@ * preview the different cards of a note.""" import collections import re +import copy from aqt.qt import * from anki.consts import * @@ -77,6 +78,11 @@ def __init__(self, mw, note, ord=0, parent=None, addMode=False): # take the focus away from the first input area when starting up, # as users tend to accidentally type into the template self.setFocus() + self.newTemplatesData = [ + {"is new":False, + "old idx":idx} + for idx in (range(len(self.model['tmpls'])))] + self.originalModel = copy.deepcopy(self.model) def redraw(self): """TODO @@ -250,6 +256,7 @@ def onRemove(self): return showWarning(_("""\ Removing this card type would cause one or more notes to be deleted. \ Please create a new card type first.""")) + del self.newTemplatesData[idx] self.redraw() # Buttons @@ -368,7 +375,7 @@ def maybeTextInput(self, txt, type='q'): On the question side, it shows "exomple", on the answer side it shows the correction, for when the right answer is "an - example". + example". txt -- the card type type -- a side. 'q' for question, 'a' for answer @@ -411,6 +418,8 @@ def onReorder(self): pos = getOnlyText( _("Enter new card position (1...%s):") % n, default=str(cur)) + idx = self.ord + originalMeta = self.newTemplatesData[idx] if not pos: return try: @@ -423,6 +432,7 @@ def onReorder(self): return pos -= 1 self.mm.moveTemplate(self.model, self.card.template(), pos) + self.newTemplatesData.insert(pos,originalMeta) self.ord = pos self.redraw() @@ -448,6 +458,8 @@ def onAddCard(self): t['qfmt'] = old['qfmt'] t['afmt'] = old['afmt'] self.mm.addTemplate(self.model, t) + self.newTemplatesData.append({"old idx":self.newTemplatesData[self.ord]["old idx"], + "is new": True}) self.ord = len(self.cards) self.redraw() @@ -604,7 +616,9 @@ def reject(self): self.note[name] = "" self.mw.col.db.execute("delete from notes where id = ?", self.note.id) - self.mm.save(self.model, templates=True) + oldModel = self.originalModel + print(f"newTemplatesData is {self.newTemplatesData}") + self.mm.save(self.model, templates=True, oldModel = oldModel, newTemplatesData = self.newTemplatesData) self.mw.reset() saveGeom(self, "CardLayout") self.pform.frontWeb = None diff --git a/aqt/editor.py b/aqt/editor.py index da5802ac15e..e45f7433f7b 100644 --- a/aqt/editor.py +++ b/aqt/editor.py @@ -500,7 +500,7 @@ def saveAddModeVars(self): # save tags to model m = self.note.model() m['tags'] = self.note.tags - self.mw.col.models.save(m) + self.mw.col.models.save(m, recomputeReq=False) def hideCompleters(self): "Remove tags's line" diff --git a/aqt/fields.py b/aqt/fields.py index bb909cd731e..bac331068ab 100644 --- a/aqt/fields.py +++ b/aqt/fields.py @@ -10,6 +10,7 @@ class FieldDialog(QDialog): def __init__(self, mw, note, ord=0, parent=None): + self.originalModel = copy.deepcopy(note.model()) QDialog.__init__(self, parent or mw) #, Qt.Window) self.mw = aqt.mw self.parent = parent or mw @@ -58,12 +59,15 @@ def onRowChange(self, idx): def _uniqueName(self, prompt, ignoreOrd=None, old=""): """Ask for a new name using prompt, and default value old. Return it. - Unles this name is already used elsewhere, in this case, return None and show a warning. """ + Unless this name is already used elsewhere, in this case, return None and show a warning. + If default name is given, return None.""" txt = getOnlyText(prompt, default=old) if not txt: return for f in self.model['flds']: if ignoreOrd is not None and f['ord'] == ignoreOrd: + if f['name'] == txt: + return continue if f['name'] == txt: showWarning(_("That field name is already used.")) diff --git a/aqt/models.py b/aqt/models.py index d0ecfa333b0..f9419d3664f 100644 --- a/aqt/models.py +++ b/aqt/models.py @@ -73,7 +73,7 @@ def onRename(self): txt = getText(_("New name:"), default=self.model['name']) if txt[1] and txt[0]: self.model['name'] = txt[0] - self.mm.save(self.model) + self.mm.save(self.model, recomputeReq = False) self.updateModelsList() def updateModelsList(self): @@ -92,8 +92,6 @@ def updateModelsList(self): def modelChanged(self): """Called if the selected model has changed, in order to change self.model""" - if self.model: - self.saveModel() idx = self.form.modelsList.currentRow() self.model = self.models[idx] diff --git a/difference.md b/difference.md index 8bd2c73b80e..6cd042d61ec 100644 --- a/difference.md +++ b/difference.md @@ -13,3 +13,7 @@ In the preferences, the button «Note with no card: create card 1 instead of deleting the note» chage the behavior of anki when he finds a note which has no more card. This allow to lose the content of the note, and let you correct the note instead to generate cards. + +## Anki quicker (802285486) +Those modification makes anki quicker. Technical details are on the +add-on page. From da41cbac278d1c188e996219d2f84591b2d62aeb Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Sat, 6 Jul 2019 18:40:05 +0200 Subject: [PATCH 63/68] Allow empty first field --- addons/46741504/LICENSE | 674 ++++++++++++++++++++++++++++++++++ addons/46741504/README.md | 20 + addons/46741504/__init__.py | 8 + addons/46741504/ankiweb | 3 + addons/46741504/firstEmpty.py | 224 +++++++++++ addons/46741504/meta.json | 1 + addons/46741504/zipping | 2 + anki/importing/noteimp.py | 95 ++--- aqt/addcards.py | 3 +- aqt/addons.py | 1 + aqt/preferences.py | 2 + designer/preferences.ui | 14 +- difference.md | 1 + 13 files changed, 1000 insertions(+), 48 deletions(-) create mode 100755 addons/46741504/LICENSE create mode 100755 addons/46741504/README.md create mode 100755 addons/46741504/__init__.py create mode 100755 addons/46741504/ankiweb create mode 100755 addons/46741504/firstEmpty.py create mode 100755 addons/46741504/meta.json create mode 100755 addons/46741504/zipping diff --git a/addons/46741504/LICENSE b/addons/46741504/LICENSE new file mode 100755 index 00000000000..9cecc1d4669 --- /dev/null +++ b/addons/46741504/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {one line to give the program's name and a brief idea of what it does.} + Copyright (C) {year} {name of author} + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + {project} Copyright (C) {year} {fullname} + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/addons/46741504/README.md b/addons/46741504/README.md new file mode 100755 index 00000000000..840b08200ea --- /dev/null +++ b/addons/46741504/README.md @@ -0,0 +1,20 @@ + +## Effect +* Allows to add a note with an empty first field. +* Allows to import notes without having to select a position for the + first field +* If there are no first field, duplicates are not checked when you + import notes from csv, txt, etc.. +* In the item import window, in the list of fields "maps to" does not + appear anymore. This allow to use keyboard to find quickly the wanted field ! + +## Links, licence and credits + +Key |Value +------------|------------------------------------------------------------------- +Copyright |Arthur Milchior +Based on |Anki code by Damien Elmes +License |GNU AGPL, version 3 or later; http|//www.gnu.org/licenses/agpl.html +Source in | https://github.com/Arthur-Milchior/anki-empty-first-field +Addon number| [46741504](https://ankiweb.net/shared/info/46741504) +Support me on| [![Ko-fi](https://ko-fi.com/img/Kofi_Logo_Blue.svg)](Ko-fi.com/arthurmilchior) or [![Patreon](http://www.milchior.fr/patreon.png)](https://www.patreon.com/bePatron?u=146206) diff --git a/addons/46741504/__init__.py b/addons/46741504/__init__.py new file mode 100755 index 00000000000..3f56c4c7e6e --- /dev/null +++ b/addons/46741504/__init__.py @@ -0,0 +1,8 @@ +""" -*- coding: utf-8 -*- +Copyright: Arthur Milchior , +Based on Damien Elmes 's anki's code +License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +Add-on number 46741504 +Feel free to contribute on https://github.com/Arthur-Milchior/anki-empty-first-field. +""" +from . import firstEmpty diff --git a/addons/46741504/ankiweb b/addons/46741504/ankiweb new file mode 100755 index 00000000000..6b74dc023f7 --- /dev/null +++ b/addons/46741504/ankiweb @@ -0,0 +1,3 @@ +Allows notes to have an empty first field. This can be used both for importing, and for creating cards. + +For more information, please go to https://github.com/Arthur-Milchior/anki-empty-first-field \ No newline at end of file diff --git a/addons/46741504/firstEmpty.py b/addons/46741504/firstEmpty.py new file mode 100755 index 00000000000..faa6e33f820 --- /dev/null +++ b/addons/46741504/firstEmpty.py @@ -0,0 +1,224 @@ +""" -*- coding: utf-8 -*- +Copyright: Arthur Milchior , +Based on Damien Elmes 's anki's code +License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +Add-on number 46741504 +Feel free to contribute on https://github.com/Arthur-Milchior/anki-empty-first-field. +""" + +from aqt.qt import * +import cgi +import unicodedata +import os +import re +import traceback +import zipfile +import json + + +import aqt.forms +import aqt.modelchooser +import aqt.deckchooser +import anki.importing as importing + + +from anki.consts import NEW_CARDS_RANDOM +from aqt.addcards import AddCards +from aqt.utils import tooltip, showWarning,askUser, getOnlyText, getFile, showText, openHelp +from anki.utils import * +from anki.importing.noteimp import NoteImporter +from aqt.importing import ChangeMap +from anki.lang import ngettext, _ + +def myAddNote(self, note): + note.model()['did'] = self.deckChooser.selectedId() + ret = note.dupeOrEmpty() + if ret == 1: + tooltip(_( + "The first field is empty.")) + if '{{cloze:' in note.model()['tmpls'][0]['qfmt']: + if not self.mw.col.models._availClozeOrds( + note.model(), note.joinedFields(), False): + if not askUser(_("You have a cloze deletion note type " + "but have not made any cloze deletions. Proceed?")): + return + cards = self.mw.col.addNote(note) + if not cards: + showWarning(_("""\ +The input you have provided would make an empty \ +question on all cards."""), help="AddItems") + return + self.addHistory(note) + self.mw.requireReset() + return note + +AddCards.addNote = myAddNote + + +############## +#Import +########## + +def mappingOk(self): + return True +NoteImporter.mappingOk = mappingOk + +def importNotes(self, notes): + "Convert each card into a note, apply attributes and add to col." + assert self.mappingOk() + # note whether tags are mapped + self._tagsMapped = False + for f in self.mapping: + if f == "_tags": + self._tagsMapped = True + # gather checks for duplicate comparison + csums = {} + for csum, id in self.col.db.execute( + "select csum, id from notes where mid = ?", self.model['id']): + if csum in csums: + csums[csum].append(id) + else: + csums[csum] = [id] + firsts = {}#mapping sending first field of added note to true + fld0name = self.model['flds'][0]['name'] + fld0idx = self.mapping.index(fld0name) if fld0name in self.mapping else None + self._fmap = self.col.models.fieldMap(self.model) + self._nextID = timestampID(self.col.db, "notes") + # loop through the notes + updates = [] + updateLog = [] + updateLogTxt = _("First field matched: %s") + dupeLogTxt = _("Added duplicate with first field: %s") + new = [] + self._ids = [] + self._cards = [] + self._emptyNotes = False + dupeCount = 0 + dupes = []#List of first field seen, present in the db, and added anyway + for n in notes: + for c in range(len(n.fields)): + if not self.allowHTML: + n.fields[c] = cgi.escape(n.fields[c]) + n.fields[c] = n.fields[c].strip() + if not self.allowHTML: + n.fields[c] = n.fields[c].replace("\n", "
") + n.fields[c] = unicodedata.normalize("NFC", n.fields[c]) + n.tags = [unicodedata.normalize("NFC", t) for t in n.tags] + ###########start test fld0 + found = False #Whether a note with a similar first field was found + if fld0idx:#Don't test for duplicate if there is no first field + fld0 = n.fields[fld0idx] + csum = fieldChecksum(fld0) + # first field must exist + if not fld0: + self.log.append(_("Empty first field: %s") % + " ".join(n.fields)) + continue + # earlier in import? + if fld0 in firsts and self.importMode != 2: + # duplicates in source file; log and ignore + self.log.append(_("Appeared twice in file: %s") % + fld0) + continue + firsts[fld0] = True + if csum in csums: + # csum is not a guarantee; have to check + for id in csums[csum]: + flds = self.col.db.scalar( + "select flds from notes where id = ?", id) + sflds = splitFields(flds) + if fld0 == sflds[0]: + # duplicate + found = True + if self.importMode == 0: + data = self.updateData(n, id, sflds) + if data: + updates.append(data) + updateLog.append(updateLogTxt % fld0) + dupeCount += 1 + found = True + elif self.importMode == 1: + dupeCount += 1 + elif self.importMode == 2: + # allow duplicates in this case + if fld0 not in dupes: + # only show message once, no matter how many + # duplicates are in the collection already + updateLog.append(dupeLogTxt % fld0) + dupes.append(fld0) + found = False + # newly add + if not found: + data = self.newData(n) + if data: + new.append(data) + # note that we've seen this note once already + if fld0idx:firsts[fld0] = True + self.addNew(new) + self.addUpdates(updates) + # make sure to update sflds, etc + self.col.updateFieldCache(self._ids) + # generate cards + if self.col.genCards(self._ids): + self.log.insert(0, _( + "Empty cards found. Please run Tools>Empty Cards.")) + # apply scheduling updates + self.updateCards() + # we randomize or order here, to ensure that siblings + # have the same due# + did = self.col.decks.selected() + conf = self.col.decks.confForDid(did) + # in order due? + if conf['new']['order'] == NEW_CARDS_RANDOM: + self.col.sched.randomizeCards(did) + else: + self.col.sched.orderCards(did) + + part1 = ngettext("%d note added", "%d notes added", len(new)) % len(new) + part2 = ngettext("%d note updated", "%d notes updated", + self.updateCount) % self.updateCount + if self.importMode == 0: + unchanged = dupeCount - self.updateCount + elif self.importMode == 1: + unchanged = dupeCount + else: + unchanged = 0 + part3 = ngettext("%d note unchanged", "%d notes unchanged", + unchanged) % unchanged + self.log.append("%s, %s, %s." % (part1, part2, part3)) + self.log.extend(updateLog) + if self._emptyNotes: + self.log.append(_("""\ +One or more notes were not imported, because they didn't generate any cards. \ +This can happen when you have empty fields or when you have not mapped the \ +content in the text file to the correct fields.""")) + self.total = len(self._ids) + + +NoteImporter.importNotes=importNotes + + +def changeMapInit(self, mw, model, current): + QDialog.__init__(self, mw, Qt.Window) + self.mw = mw + self.model = model + self.frm = aqt.forms.changemap.Ui_ChangeMap() + self.frm.setupUi(self) + n = 0 + setCurrent = False + for field in self.model['flds']: + self.frm.fields.addItem(field['name']) + if current == field['name']: + setCurrent = True + self.frm.fields.setCurrentRow(n) + n += 1 + self.frm.fields.addItem(QListWidgetItem(_("Tags"))) + self.frm.fields.addItem(QListWidgetItem(_("Ignore field"))) + if not setCurrent: + if current == "_tags": + self.frm.fields.setCurrentRow(n) + else: + self.frm.fields.setCurrentRow(n+1) + self.field = None + +ChangeMap.__init__= changeMapInit diff --git a/addons/46741504/meta.json b/addons/46741504/meta.json new file mode 100755 index 00000000000..b50e3c40534 --- /dev/null +++ b/addons/46741504/meta.json @@ -0,0 +1 @@ +{"name": "Allows empty first field during adding and import", "mod": 1553438887, "disabled": false} \ No newline at end of file diff --git a/addons/46741504/zipping b/addons/46741504/zipping new file mode 100755 index 00000000000..f8bc62b44fe --- /dev/null +++ b/addons/46741504/zipping @@ -0,0 +1,2 @@ +rm ????.zip +zip -R emptyFirst.zip *py README.md LICENSE config* zipping ankiweb --exclude \*~ \ No newline at end of file diff --git a/anki/importing/noteimp.py b/anki/importing/noteimp.py index f51d1c4950f..e74eef54b67 100644 --- a/anki/importing/noteimp.py +++ b/anki/importing/noteimp.py @@ -48,7 +48,7 @@ def __init__(self): class NoteImporter(Importer): """TODO - + keyword arguments: mapping -- A list of name of fields of model model -- to which model(note type) the note will be imported. @@ -81,7 +81,7 @@ def fields(self): return 0 def initMapping(self): - """Initial mapping. + """Initial mapping. The nth element of the import is sent to nth field, if it exists to tag otherwise""" @@ -97,7 +97,7 @@ def initMapping(self): def mappingOk(self): """Whether something is mapped to the first field""" - return self.model['flds'][0]['name'] in self.mapping + return (self.col.conf.get("allowEmptyFirstField", False)) or self.model['flds'][0]['name'] in self.mapping def foreignNotes(self): "Return a list of foreign notes for importing." @@ -124,7 +124,8 @@ def importNotes(self, notes): else: csums[csum] = [id] firsts = {}#mapping sending first field of added note to true - fld0idx = self.mapping.index(self.model['flds'][0]['name']) + fld0name = self.model['flds'][0]['name'] + fld0idx = self.mapping.index(fld0name) if fld0name in self.mapping else None self._fmap = self.col.models.fieldMap(self.model) self._nextID = timestampID(self.col.db, "notes") # loop through the notes @@ -147,55 +148,57 @@ def importNotes(self, notes): n.fields[c] = n.fields[c].replace("\n", "
") n.fields[c] = unicodedata.normalize("NFC", n.fields[c]) n.tags = [unicodedata.normalize("NFC", t) for t in n.tags] - fld0 = n.fields[fld0idx] - csum = fieldChecksum(fld0) - # first field must exist - if not fld0: - self.log.append(_("Empty first field: %s") % - " ".join(n.fields)) - continue - # earlier in import? - if fld0 in firsts and self.importMode != 2: - # duplicates in source file; log and ignore - self.log.append(_("Appeared twice in file: %s") % - fld0) - continue - firsts[fld0] = True - # already exists? + ###########start test fld0 found = False#Whether a note with a similar first field was found - if csum in csums: - # csum is not a guarantee; have to check - for id in csums[csum]: - flds = self.col.db.scalar( - "select flds from notes where id = ?", id) - sflds = splitFields(flds) - if fld0 == sflds[0]: - # duplicate - found = True - if self.importMode == 0: - data = self.updateData(n, id, sflds) - if data: - updates.append(data) - updateLog.append(updateLogTxt % fld0) + if (fld0idx is not None and not self.col.conf.get("multipleNoteWithSameFirstFieldInImport", False)):#Don't test for duplicate if there is no first field + fld0 = n.fields[fld0idx] + csum = fieldChecksum(fld0) + # first field must exist + if not fld0: + self.log.append(_("Empty first field: %s") % + " ".join(n.fields)) + continue + # earlier in import? + if fld0 in firsts and self.importMode != 2: + # duplicates in source file; log and ignore + self.log.append(_("Appeared twice in file: %s") % + fld0) + continue + firsts[fld0] = True + # already exists? + if csum in csums: + # csum is not a guarantee; have to check + for id in csums[csum]: + flds = self.col.db.scalar( + "select flds from notes where id = ?", id) + sflds = splitFields(flds) + if fld0 == sflds[0]: + # duplicate + found = True + if self.importMode == 0: + data = self.updateData(n, id, sflds) + if data: + updates.append(data) + updateLog.append(updateLogTxt % fld0) + dupeCount += 1 + found = True + elif self.importMode == 1: dupeCount += 1 - found = True - elif self.importMode == 1: - dupeCount += 1 - elif self.importMode == 2: - # allow duplicates in this case - if fld0 not in dupes: - # only show message once, no matter how many - # duplicates are in the collection already - updateLog.append(dupeLogTxt % fld0) - dupes.append(fld0) - found = False + elif self.importMode == 2: + # allow duplicates in this case + if fld0 not in dupes: + # only show message once, no matter how many + # duplicates are in the collection already + updateLog.append(dupeLogTxt % fld0) + dupes.append(fld0) + found = False # newly add if not found: data = self.newData(n) if data: new.append(data) # note that we've seen this note once already - firsts[fld0] = True + if fld0idx:firsts[fld0] = True self.addNew(new) self.addUpdates(updates) # make sure to update sflds, etc @@ -213,6 +216,8 @@ def importNotes(self, notes): # in order due? if conf['new']['order'] == NEW_CARDS_RANDOM: self.col.sched.randomizeCards(did) + else: + self.col.sched.orderCards(did) part1 = ngettext("%d note added", "%d notes added", len(new)) % len(new) part2 = ngettext("%d note updated", "%d notes updated", diff --git a/aqt/addcards.py b/aqt/addcards.py index 19c00d83fcb..1e27349722d 100644 --- a/aqt/addcards.py +++ b/aqt/addcards.py @@ -171,7 +171,8 @@ def addNote(self, note): note.model()['did'] = self.deckChooser.selectedId() ret = note.dupeOrEmpty() if ret == 1: - showWarning(_( + tellCardEmpty = tooltip if self.mw.col.conf.get("allowEmptyFirstField", False) else showWarning + tellCardEmpty(_( "The first field is empty."), help="AddItems#AddError") return diff --git a/aqt/addons.py b/aqt/addons.py index 70275bee3db..957356fae84 100644 --- a/aqt/addons.py +++ b/aqt/addons.py @@ -901,6 +901,7 @@ def __hash__(self): """ Set of characteristic of Add-ons incorporated here""" incorporatedAddonsSet = { + Addon("Allows empty first field during adding and import", 46741504, 1553438887, "7224199", "https://github.com/Arthur-Milchior/anki-empty-first-field"), } incorporatedAddonsDict = {**{addon.name: addon for addon in incorporatedAddonsSet if addon.name}, diff --git a/aqt/preferences.py b/aqt/preferences.py index 32901cce08c..b893c7f8884 100644 --- a/aqt/preferences.py +++ b/aqt/preferences.py @@ -272,6 +272,8 @@ def updateOneOption(self, name, default=False, check=True, sync=True): storeValue[name] = value extraOptions = [ + ("allowEmptyFirstField", ), + ("multipleNoteWithSameFirstFieldInImport", ), ] def setupExtra(self): diff --git a/designer/preferences.ui b/designer/preferences.ui index 0009a7bf405..38a811699bf 100644 --- a/designer/preferences.ui +++ b/designer/preferences.ui @@ -7,7 +7,7 @@ 0 0 423 - 508 + 552
@@ -20,7 +20,7 @@ Qt::StrongFocus - 0 + 3 @@ -489,6 +489,16 @@
+ + + + Allow empty first field + + + true + + + diff --git a/difference.md b/difference.md index eeaf6f0f9f3..7db13b40541 100644 --- a/difference.md +++ b/difference.md @@ -2,3 +2,4 @@ This files list the difference between regular anki and this forked version. It also lists the different options in the Preferences's extra page. +## Allow to keep first field empty (46741504) From a7d2602fd5d86815b2654ef60ca459f78282b7ea Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Mon, 17 Jun 2019 14:19:04 +0200 Subject: [PATCH 64/68] Merge multi column --- addons/2064123047/LICENSE | 674 +++++++++++++++++++++++++++ addons/2064123047/README.md | 43 ++ addons/2064123047/__init__.py | 1 + addons/2064123047/ex.png | Bin 0 -> 126120 bytes addons/2064123047/icons/frozen.png | Bin 0 -> 1111 bytes addons/2064123047/icons/unfrozen.png | Bin 0 -> 969 bytes addons/2064123047/js.js | 70 +++ addons/2064123047/meta.json | 1 + addons/2064123047/multiColumnEdit.py | 211 +++++++++ addons/2064123047/user_files/css.css | 7 + addons/2064123047/zipping | 2 + addons/3491767031/__init__.py | 257 ++++++++++ addons/3491767031/meta.json | 1 + anki/collection.py | 1 - aqt/addons.py | 2 + aqt/editor.py | 182 +++++--- difference.md | 15 + web/editor.css | 2 +- web/editor.js | 60 ++- 19 files changed, 1448 insertions(+), 81 deletions(-) create mode 100755 addons/2064123047/LICENSE create mode 100755 addons/2064123047/README.md create mode 100755 addons/2064123047/__init__.py create mode 100644 addons/2064123047/ex.png create mode 100755 addons/2064123047/icons/frozen.png create mode 100755 addons/2064123047/icons/unfrozen.png create mode 100755 addons/2064123047/js.js create mode 100644 addons/2064123047/meta.json create mode 100755 addons/2064123047/multiColumnEdit.py create mode 100755 addons/2064123047/user_files/css.css create mode 100755 addons/2064123047/zipping create mode 100644 addons/3491767031/__init__.py create mode 100644 addons/3491767031/meta.json diff --git a/addons/2064123047/LICENSE b/addons/2064123047/LICENSE new file mode 100755 index 00000000000..f288702d2fa --- /dev/null +++ b/addons/2064123047/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/addons/2064123047/README.md b/addons/2064123047/README.md new file mode 100755 index 00000000000..836ba0634b8 --- /dev/null +++ b/addons/2064123047/README.md @@ -0,0 +1,43 @@ +# Advanced Note Editor +## Description +This Anki add-on merges two add-ons: +* [Multi-column note editor](https://ankiweb.net/shared/info/3491767031) by HSSM. This add-on does not seems to be debugged anymore +* [Frozen fields](https://ankiweb.net/shared/info/516643804) by Tiago Barroso and Aristotelis P. + +Those add-ons seems to be incompatible, and creating a third add-on seems to be the easier way to have both properties. + +![Example](ex.png) + +### Multi column note editor + +Use multiple columns in the note editor. You can easily change the number of columns shown using the *Columns* field added by the add-on, and you can have a different number of columns for different note types and window types (browser, note adder, etc). + +If you have any fields that take up a lot of space, you can configure them to take up an entire row. + +Note that the configuration is saved in the collection. Thus, the configuration should be shared from a computer to another one. + +### Frozen fields +Anki supports '''sticky fields'''. A sticky field is a field whose value is not deleted when you switch to a different note. This can be very useful if you are making many notes in which a field either has the '''same value''' or changes very little. + +Unfortunaly, marking a field as sticky is quite complex and breaks your workflow. Frozen Fields allows you to conveniently mark a field as sticky (freeze) or not sticky (unfreeze) '''right from the note editor'''. It’s much more convenient than the default way. + +To freeze/unfreeze a field, just '''click the adjacent snowflake''' or use the corresponding '''hotkey''' (F9 by default). A blue snowflake means that the field is frozen and a grey snowflake means that the field is unfrozen. + +## Warning +If you install this add-on, uninstall the add-on Frozen Fields and Multi column note editor. The result otherwise would be unpredictable. + +## TODO: +More beautiful buttons. + +## Links, licence and credits + +Key |Value +------------|------------------------------------------------------------------- +Maintener | Arthur Milchior +Based on: | Anki code by Damien Elmes +and Multi-column| [Multi-column note editor](https://ankiweb.net/shared/info/3491767031) by HSSM and +and Frozen field| [Frozen fields](https://ankiweb.net/shared/info/516643804) by Tiago Barroso and Aristotelis P. +License | GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +Source in | https://github.com/Arthur-Milchior/anki-Multi-column-edit-window +Addon number| [2064123047](https://ankiweb.net/shared/info/2064123047) +Support me on| [![Ko-fi](https://ko-fi.com/img/Kofi_Logo_Blue.svg)](Ko-fi.com/arthurmilchior) or [![Patreon](http://www.milchior.fr/patreon.png)](https://www.patreon.com/bePatron?u=146206) diff --git a/addons/2064123047/__init__.py b/addons/2064123047/__init__.py new file mode 100755 index 00000000000..c7c7eedfb4c --- /dev/null +++ b/addons/2064123047/__init__.py @@ -0,0 +1 @@ +from . import multiColumnEdit diff --git a/addons/2064123047/ex.png b/addons/2064123047/ex.png new file mode 100644 index 0000000000000000000000000000000000000000..bfd2b748ab997530af357c4de0506abedb1488ba GIT binary patch literal 126120 zcmeEu-LUCaLh79xpZlEi zoO7Sw`wu){Hn3T1u32NuF|O+xD?~v~91R&C`Ocj?Xp$15ukYMJ@Vay7!4t%L;2T*3 zn=A0+zN4_DG9n`4%#!@#ojas=Bt>5;yXo%CA-G|$O&=V}z7<0J{^dRC`-k+1$Sw?% zp0?urC-dL9c79EI@^uzX)=l!ey!!S=q$EQk=7$K%`StJmeXk3f-dqN{i@~+_3aB>> z3SB6wS+7}#dLm0nOB-I(X1u+J{I?$p1or|fhC;mm_Mf+R9VEP!G;Kuq``f`471VEr zYsP7JCGI}NRuKA|kEZOhg}?dw&!v8vSS*{`ma#6`|E-5V%I7gJ&sHMJO!|Kx4+?JY z64x63vojymGYmUFTF+GOjuog~@rVsZZV#F+rTt?#=hO;0BP;KQhlf27+s>U{zI@r} zbG5b9P$T`%Hr&Y%rcBfAU)Q!E(~kzP{6eh}@Iv16|7*q~2k%X#6HkmB;rH2duA~eu zyou2HYdm=v_biLQpY%Z?+pTFO7-?h^F&Hg#|Coj&Rnm)*6%S2a|9pHL931zHbN9u6 zKKGYs#1~f6-@2Qvsfsf%D5>zb0}1(O~q$7b0?;_91`u zEklSOJFijFwB$uWQSibuSINIR)Ap7?3-=MwWz6hWqr^>g zS#^S$)ZojPSJRYVQ1p8vE28LI2fl{O8lFSE0;iIY-wW*O^)ydiufz6E65kgX5pyD{ z;QFmhAyo`atMsk)%wXdD;La5$I@5ITt>~g0N_sak7LTUe(JUzx=Y0^8M( zG2JmVzDM0O>*>A%zUBLkH`ghpZb2zZx_oLve!rQ2WJ|_uj(iTj(l*bDt6r=+hb14= zBZWq~4%i83=VVdT&6S(0BE>v+wloAylKMUAY_VsN^KRO_&ayZ?a^IU2Pyc;&7 zKCWfUuS?={wnUTebtK2PD@ty-p6Ysib-EzpPp6bi=+H$*UsO$gisA51m(HF$=R^PRW^8CNBO>14}-A%bK#fjas^%{Mo87oFPXRIJLv)oUK?c1I( z*B`XFucvlSop3F9iNu8$6xD{V&8l^Y!bC9motoa>)u!{>F02y9m~Flq5-0gU#CP&@ zME+*q_oi2qeX-E&m9C?PeVmHw=5z%;o&`*!EMS_*>gQHLF)_Q@rh{yl>KcGXhE5>E-!) zdU3<=EnuRWLH&pE>0xj?5Mu9yHilk4L2o=lz=Iwg9bw78_~y7ikn&sXbF()7tnbZL zo%6OB@A9-|G1^<;cX+`TuTjZSE7qNTB4OgcHuS~z=KAX9e9)JvZl^R$>;v-d&E>KD zo_Gqk{io|&1BVW(Ww{pkE`6+P>=v!R{=Kbb`<-LX9{u}-u4kYR&l1>2Rdr6A4y|Zh zkx$*P&(}447X1)Tc4tltiW`IOxyh}>Uz4J-&h=8o@A?vw*& z=FqUPs%eV?U<0S4O7h;v{al0GE8$?Bqr+VKV3Xkc1QB_+kt@=ENdj1$mE1TKLC{J@E;>a`p~bCz{A>o{nCPRf19Xs>o0wF8Ej z4fe!M?7n%pF?GM%sb2ZGN}l-1eq6)!>hgGSHFmuZ+8QWgL6vIPg0wX1x;I;+{c@FK zeJK`)>etIbqZfJkqVW_yj_!H8Wc0wTOra0|5l zI$uj*phUADRTO8n#tSg~$P?t=DWeW^g6d_dv~1d*#R(5y9wgys0fV7;C9Slc;cT#X zBa+C%fy(r&s_MEM#~+`jj3Ut=F~t;jzdU}2@*%~sOq$>0XIF4UtD!Pj+x+KE3)a^T zKBsf8bc3EX`^l&hZW;6mr=JFR!eG=m)KW26U!1~*(yyenrs2-F3&HQKF|;0L#4#GX zvP`0scAoq~zvTg2?<|za;mIYDiEg{gjCH4oHq@$Fl1=b)s>?LTgtp`5VP`Owp(-kK zo_E#7UY(vpHqh@})`7qpGJtM9U`e z2k$Pl&rswf&GuN0IK|mr4V&LRoXyoa{$7Ouzw-&oK_>*;iQ4{hDVT(fhly962^N^` z*wbp7E2b{jka*-$W;%#2xgPr=E70mAUqfs0YQp*b0Ui%UMMX(Tq?@3%STzGS37C(U z8hnUaeh03}?=HZ6(D90I))*1ypF`G}lEWR3YMB(_lkgsQaar#y&hgE_8y+;6XwooV zY#d6jyX+d8oEek{SLeo1?BV+N%$M8ag*+E%#pk_!Q{2Y9W=#;a<@9z&_>qtylmj0v zBo+FC?Kq)EqKl)+%n(?Jv|ImzJ&gOXeS%j#&K98P`Gf7$z%P zM$^;NqwBtgBV}3=Kp^U-fHM-g62X2M6@H_H$C#;c$q;@PU!UvX)br@F5SIPotHfH} z`SE&LK=NZHP8&iPaUke}LfE}gwofpD^$td@N*VY0UXtTTb8=0ss!)m3Gl1_VqZ z48cnlNmD61g=?nkLNVLzyU&4baA4;t4nwVD5SGMjuf<|qKe_~l3?dNP-3i=fsy`66 zAv5YA+%&g1QA)e%gAA^UrW{?aW@Uwapo?f$+KOdhQWo+PV`y67r`EH(K&&!xBq2GD zb-#X&a-L9kUY%*QTQOnM@TmRg7ctyau3*Q70N)k)_bWFkN44N!r%b3>2|yx!BS!CV zOd3J8Y9b=n|AklT)h4I)^Z|Eso@`{qe&G|}r^HEMVj@2@Ew_h3NBbDP!GvrAZ$4P2 z+X~TCN;`EZ=1RbDALX>p+w3FpLdN@sbIl-l+y{}#SQ%tHbuyhV@(qAooE6Ir;YJFcG0rl|>k%m``OlR^Ty2!BfXqED=@ zX!8!ejEB9){ZlI{aQh@v7|Dh>L*)g|jjq?0^~>Fv9z|;t^LwyIq|~l_5k-QT#O<#DJ=Q1!{@Edw@)>@RzD0MyQLUYWWZl?oRz88ip4C_ydDPJbGz0^(S zTx#RZtXdr)_|S;(37m}C*odyd*N+i_VjSb6p{_q z>L5q4hFr%vII};$eN>Uw&ETlWl7hMN^u{!v&WSP=`lNIzh~W1+mv4hI@t1qZMvuF^ zJO~>N?yfPjImPyC$uwk@LOu^=vYO6ZF3DA+y-4<1o;h%`O_T3hjmj@F^8}s)zNz6K z7SeBLq;!4GjnjHHwTQ;ENd_Y=Kdslo?ey0Jwb1w;9^ZdHMn4I8*FmQttD2W~yHenZT?|c(bFsn(xsHQr{M%aGUG?kmvkZAvj`X52 z7`PF~cINYE!cW#$*Hm6}&Ct&0EAAB8&5U6qMFmyusEJO^I91b9~}n1qH*eKsl^ z+TUpkzUG4;1f{rL*YXA*q6en-6;!ldvxU4Hh|R71>Zr2$uZ_{)o9*kYpv23*~J&nTtw>O^Zm zI%Xk1h6MD7^x(k9fW`7X>b3yZI;0f68WT&F}=DPTQdG^1|Ggw?AWbE;!%DM9YdI1H< z6KA^w6fng9>fJx~%%6wlN&9^zraS|ibYe}Q*7{%e)IU1|e)IDnEjb?GQ>(?*7<00j zDALIxn)=6p|E)>*c_x!Cz@MDeYmw_1?`>ShKl6{w@_+Ro%m%x`T}xiBWsC_CrT@}i zeED0+bt=ZalmFGBe+~yYRgg+Q@vXk~egBx@e|cg__QTTr&H8NDp51@0z`y(-ehAao!nCkKm0E=ADSr%gSNnzE0h?Oo{q%N0WYPuxzXObfmjtK@BJ?^;*?t6lu_o)7XFU%y)Y z+!*TaRMFO+%@heXj@O_*FLaWA%B*_{l9@nP1T=hQ1%=2WWA*D+3_+PBcJr(coKj{d zAOk5e`u=OYx89@2)wD115aeB4w)6G!=>o4_y`q~@wp|(&ym)sU7tjPM=w-4xm0-z-!xyo3k_{^MnQfan$$V% z`W~lBM)$OAP#}wi8U5km6Xx@=f_RBgX<#tmO=cF4~ki}SVn4HqDD;cX~Ivl*Pz{~H_hd*Jsf z?CBLa$>}uUIZy}|&B^uz^IBau3JCB;JfhZ&aLObZn6#@|cs}%QF?@A$j_x(`>-HOt zuGfwr=GUyU>8~a6y<`np{G#+ZdTnQ>%JxXElya_?jzAnG^G$mI_Hhq=af*B40n;Xo z+ORX%QWC#5qrCheM!rW)-EY)Uxa?@S`x}q%myXvoFX;`!*4wXB6n@lt>(8_?u#g}~ zn!Y~Ruy3g`LoT?_>-1X@4 z(HYOcSTEn_lcHfJV(*_rAndZce9qi(Mn1I)k~+07=^m43YYBP{yOlFyAMo1|>Ol^M zLHyz@084ITw#JcvR^^}A&A%8)2KvJ!&a3nnTJG-dU6|2EDg2%;1*(OmQw6mCDKGdS z$zx@<5T8pohn^kl$tAERE2j&-rZyZSwp!(8-)0?pIV{J+IqCTNN^E*gkWjd0tjefm zlZ-~#ELsWIQSsK#ip)BU*3Zrd)=*$f+ANT9hA^}NdTUkf*=$<-1_&g~^Ve3K%~fIL z^wx;H;M$7Z1S!MkyCH^5&z&}h1JRd=_*@vG^?c6PG%X9`+;vR0gZ5vWiOtuz?$1wU z*Fap8&*xV2@yELOCWMEf-&cn_(YR(G?=q2y{em5s3M^7xGL4io*X^+7T9G)9D#}z} zgE=Dh*vRAq5fMOq1{~z8m)j<2dpiKC^iN0m*KcMS_xgg3D-FtzZNL9|8e?=Ghu2_) zRK}CXVjUk3Kni8f-GYfAa$=*DFNjap_9Q||tNQGT*^Cg|dAmON9Jb?be=U=9xPeZ{ z+UU_sC&E&^1=*SOT<7wx>PyYT$_D!45@hKmrFcynH&GcEd@ih`1Nx}e$wd=)X1{y7 z4u4kmPr;R5G1G(Ai3FbdtN(XU{6?~5Rp|5JJ#I_^qR*;=- z^r4!>hf0mlY2(KM)A<>JZG9Bp3U=ud%2m^SjO?9AjJK`{x*nU1m6PNR_~6MDW^PbkE)wZFUIzMk?AKL)v3Z<^zn0m$bUZ0dH2y$+fwK61Z9I}}{k_1taG zL1MZtsoJkUmMeFLb%37ldU4h}_j62?Tmnoe2OXWy_s2^F#VRTb4Vq4sI;oS~zmD^N z91I}%X7~Y;wUWrX^_T_O8p`J*Q9L<`4s$!WuX+7DvJXG0BznkbrUV?-yB|}_Bp!gY z-L|Cp5q7rR9^|;Td)Lw!$vLHLpC~l24lZpx4dLY}lERq%+f$YUo75HCqswR7 zfqkULC4Di$jx zGMp0V&#>LNbjA}gqEd`{1XggF{H9SWVw~Uz?CNvJ{eX# zJPu6zoDk{Ma60dKey6DZNGde?Ye@?-HZ2dpgoSBhbpY5j*P4pBL@tzL$6?!aql0ZP zr$*;B*XY}#DhJXcDZ$2{!_vV-F6!zI2h9kH@H*9 z5zq5qoZLSwTjDV(h6FM_gAqQtO0>J|J+5BUG{rEA=X1#*^HNHbZ1_OP6J3+Jnj~V4 zP}ybaLh4bf*W=w)ViUiB@cgdmulJ6;nS47Kjw#zG!JGL_uQ>>KpPL7yLfOqRnT4N( zJ0a>1Tld7W7qE=*h>R!N>dX;4PkQW9rmk4>(u_WM>BqZKw_B-oIV4u~OcX`39sL;# z>F??G&#QD_`r6iM(%jy4w~SV+JCCU(+|l+5*<0;DEbm8Qu8$NZ@iK48$vnd!#~yhv zSj2xi3;7+l5*j_=?4aG35{3dN?@RXbS+9SC>I`UEJBy~*K zWPxo88jX?7FXEGqg4q=lo||%?gX>eUJhfBjtakoIwBPDT$W~DRC&M|w)3QQQkm#fH zRzrnz_{8y6osx^3<`@V@<%W4io#bk|Bcn~9JLTLYx9bUq_{x3=13+ZF`MOMo$@8B8 zaK-PZldEah)aYv4QaI#aGct`{lnL|!+(1{FQlP)a%%S5nmg*O^P8E7?7kF~|FHYq) zs*NOloby)VGEg!Ud*1k-0VhO0YZ^bZ)KbnG>M1>h0W(k{N)&DtyuK1y^0Oj#Q;%_u z!3g!j|1xNX`P{4`IhrYgLvnrtQ>%r2R$lNbR)*hEMWh2d)xVn2YDrzs7REI<5UE4C zCgA{eULm3G4BuqQ6qpNsRkgY0kUgV%d8C*nW^5C$pN^&aH8tBY=zHchieoyRP!!K* z-_F$;%>!y}{AD&2`GXbH$r62RZ{2&6rnx3cN$2reHawfzG0PDjLpLu62k-OCzc_3g zQ`P!Vl{eU*6XGP^X>YGXzLhx;LElQV24^JxWAI~Gq|)4O2a9GpY1ra^8a`J5Oy!z3 z%aXc4rNT9f--Zt8%Xb~WZsm;qmQ&5*XbaVq6o#xF-^X$hn5)IcT%vVL=v)h|x5yEf z{S+<(_I{L0XXyHlO06mmzOr+^V-lfrqEc_>E9S0&AR<pF)`M#JKpL;y}_<*w^)& z&1d3igl^#Un6>XDVqvvS58jyrM+(vm|3-%Yq9?F)je)rM?V>s+3%ee=&-K9h8XamR z;EgEa99vgT1qGi4nizeeOC>DCyixsy9vN|5J|b7Q@hNQ5y>!XycOb6rB#1Fv(j1xZ zj1)<7Ic~mPe;}94$vCH)m@CU~Nt`ZmADI`9?TiU9U3acjcYGj7u+jQMLH(N>0esvG zDrT8Q<62A33oNPx+WJV@3yUf|L7x{9ufz0GkD6sVO>D8^X>7@lYmFo%_o5n2TuhE? z=@M8%99sgAvh%x_*yvwe1T*FCFt!Uv52*ZReueAH`1_z0qZMl>FHC44feOwhD7Wyf zNyV<_$elL%-Oqadw9mr?2^Xh}`*drpVAwy$7$la5OOBbmoz%%zEtr*Z!PuP=HA5vV zGUbDpwn1Z-+l48EPA1|2v`57}aGi6W04KWi6D;J;1Bvee_un`O;H^UjUjUq#=}xNl zE)q@3<7tKM)kMROOBdpqF(5!#j+SOZ>OHRqUbXWeDFSNwP%-_`Rz)i`HM51Q#(`Qj zMjIy8g3h-iL}tR9-Gk6=h8{qV31E0jH!*`AG{t}<+YWfBa8rwIX8H@zboCyc?Of3- z!W3&k+Ij!6ZvjY&@h~wf>8QqQs3@8wJdLU2XFmf5*$Ydi8dgUu$P445^72?OvzC?d z#L!;i4%`UYy?83%d6AlU!~CuLJv23L1g z*{wB8%oLA~Vx`Rp8|&ZbmxadezGh^2qrXdaDe@irIi`5>^6_#jRjZ^TELJ?JUP&f2 zhZ98!2JOb5&xt|foFuI=E)U=-wXHuC3wpG~n3j@o&Zl9H5yzPqm<{2ejpo9FF^KnK zoL|sn;@Np=WCap3N{($z+(+jot%pbD(HEwYm$j=E=OIb$jd2ch47ky3Fq#@xm8O+- zhEA@GYcb&q!<*}nr)kdP8cWV95n&f77TeX)5nWV&XXh1ELj9o%p=7I$5LW`6C90gn zOal>N8Y6S4l8jg>NCAhmR*R=>f}Z2api*tVu=fnLIQdHpuq;YU{H(Bj{)kfKNC$cf zqN(f%ja0TWI!kq>27gyN+sBPaJ;S90WDBW9>UOWT#~<3CxAu8TteidS$K^^_OWSR| zo6IGt_4pk>s@lWEVB#&!16$b9NG4vo75fMtf6udnp`ZH;g2#$&jlHq%Fg`dLY4i=c zC_@9CpeoecB((78sy~KuMXl9UL^=&PNdIm0t&pLJ%p%L3^ik?vG`kIVUo76_gS#-q zU@Dd88#4D|N6Ff=x($O~7}CCesWxk!jt(`rFL-kaiF-pwP_k3n8;yAd==|=75FXxG zH@PbEUP!TYZHo9j>j$p2n89%!*SXa+Nm#U;F$wL5x4JzBZDF3LOqZ)MtIkSHnf8}Q z=?uU%O~i?xb&mAH=h?woM)u7VJ}pl;LH+NzOc%;s%zni^Zr(T1`B%Sx75qGd zx)M$s#bly0_V)BhuqyqI`+1nBm_>U>+9LYXR~g5>;T#k^JDjcxFLy=mZ zpB>to$O1-?3y#)Wqm*u<7%O8rn5HWDVaSH%*?G=cV zbjh#CdyLUKjxeHs5-oHp5sk6gu!C<72p2t@uk2eHOg^T(8_DWBM_W zA~aOzphlsKE3-)gMqx0bALN|p#pZsrYRBqdhK~k`Xg+ATk zX^V}xHY6L-bl279gld`^M$|^IJoQO(RnQ-ETTaU15Ga7gVc$JLP^*X zSJ~;K}DRpG&#|KY>LPMnrv31TfZEWJ&kxK0WGW?LX)S)r4#A zM~DiCaPsO{UAgUW{wpg@^XAKk-$Fwhw>Ud*u(6fdnM0k;^oB~g-N&9Gv=EE^VvW`c z=(Eksj=E$r{wmoZ4`mB%bGkU$h2Bq_r9Q@F5s8Zb!Qe4Wu@Lh+-6@y0=cYJ6<(&JR zb0TJQ3T{SqNa91>=%FGu>)0NHvU(V7YeZU24i0HCph299jKkd2;RJzgKQtwzpH zTp{W2_G5qNzf2n)QLyAAwpa6?!4W)WU^jyUTyz30^8NzTtEslJ2ytGCe;BWSJMiFt z#)k{x^jIbFmw{y!ssd?%lgpSM5LHr;zgl z)P|S+i(09+5!<&>*`?mytbE0>8yYYIzf0`AeaUq=u>)Arp20!trMz@^7!<@V)cQ+& zhV;5;U;8z`o9xc$QT~}d3-P8!0v>!X%XNBVW_YKFQJ$%M$alZ|b@~HGfwcJHmglhh z(5F{$&Ck`7tNmt?s6`eNsb;N(I44?jHW!0}A*}VUmE{UPO1Cp;@e;I02HIP?Fv5km z=X2ZM`IYZmaX=uP(<>~*40y3or>-=nh0z=2IMNFc>HWy09Ir95PhM89gMHHoLr%hG zy7n;GiNM3zX4IQ3rByYn_;7iZK1~5fUg1x$JSYq-j?(!ve#SS-A8yFGvw!)bfH48l z2fWLN3xe)P`bGkMiwD0GLleX+HYZCnG{W3zY&jd1W@>grr8W1+ zp1|=}Oa10dQZ@kP@rF~jW~=uAsa*~DpnSDYJ&Oe1Jg5yHE^n|f9Y*tZ_7{Dtiw?1D(D(~jl#~ul5n_IdfZ2X1(~-rU(Xg*XS`B8&sb9uu2s0*)jmi7 zz`qKI48V~ZIJTmqs$mQu0AHX-$*&6I0(u=W0&DC>rti-C^WQzbe~XXeLVO_PZ(GqQ zQ3MZzRy7ylC@s)50Of}DyG@INLa{SXfE_u08~T1Sy$uM`ueyU`Dp?l48hDCw{upo4 zfD?7?g2#3t3NF*?A!6I&LzJi5fN8Iw#Ux3It|MYKcra4kaJGz6M9d7hX3#i>yImTG zAi#xy9L4u)Td%u-;hYL6gD!vy<{F%nRY2lKt%c#fKHiU_iOZa*+fd>h&fpkkB5Np% zV)&g8n^_$@nYYSI?7U_9DeLP&j;Y9PIMs{HfaeEL>C0depX#&aFo5&E>ga<@f&hl9 zsFmCROR~(LQ~C9dd2n`xaB(Fq+lK($DFI2&H_y*$9?$Vi zmnZ7>8$5?4X{>A3;$@S^o$|Ek#rq{{JQT;stP9U(ZnbHE&lxq#g6B9MQ%<_5s%mMt-fD4|)o#=93nsU6Nl~ zX=rGu`v3@Uq!r}XVykP@yM747U8|P!`7Q^>bNG+yrZwjlXVh;x53AvB%FPd8u&L(q zlu1bX_aC9E1HnxfH++xCCb1*+SLa?t_+A5WMc*nlPT}^KTe|RPvS6qhXbj6-xJ`~V-sK!umt!GUm!#>C2O@} z>mAwxIUSq>>8rVC3r`orTfrn~&!cUwtoq`yK$=VdO5KL&2dEGLgC6>02y|ueQ<}Pq z9q0!WRc_@MU~ZS=^wC25qIQ0>eZM(}rNWh0#-CBK=*uVV#dCV&US z(OWT0S)5vGQz)aDNVx4tEq8(9N$~2Yl5p^0fIe56^)L(f8BJ*X9oaEpuzf{pyPtq z9Q(#~fM-1bl}+C)*}9Ufga3Rr<^jh$(qiC>7T^%l+3Vk@7>Dc)1wb2!rS}Y@jL}BU zV>2gVfOq?AUNg|)NN(uU1Em?hpwA^hV0yGR$rWLXy_z!gE1VSMi^%j}g$OaOvQZnf zDPh$gd0@|&h+`-aqiUy{Pl5NKd0(bex(?KPltVm@2gaSmzL)mM(T{y=7O!B*wz+&p z@d)((`~5w3VTLEnaIQ_%W6+<8cvUaRAFQQ7R-k6tU}l zbOzz?b(2{@8U%jZ1^m%)U}B$QoKGpq@VJd~;a22$p5~_O7#vF#@<9fG6jYJCEDvxh ziVh9jX<$dJT67m2F@Mn#i@tlhaC6Ny;n)4`WNR!ev43qDF_n_zGYVy@=gCfYEYqWz zzO7Ig15Ky7+{5>$qhq1IVUvScF)e42Qo1Q5K-IMT>{Wxhgm8*IHv!c-pVygPrG(_; zct^Yxr0KqBSZrV)bC&T_HfmqM_|zm_4e;$p(wh>>jiJYrfUKKQvwU&KkL+i;`N-tp z8POY;6X1p9^HUvT)pXqh(XIfdt?zr5V8mtU>GN#1(tISSNWE(l$tb zYe++Dz=YkE00pPBhvN3TXlfi;n!e{3&anD(6vGv%Ug`ll`mbiwxh2LL8fD*i#a99&$7no zc#t#IO_MHDf4%6BQB=K*oclSdU*2`}?K3ZkZ|AMBD-Y{#X>GPKn5Z>WEt+yxEhvwW zc7yRpY+MbI6(Zla8!+)%MUIWl;=vm(#mhx<@ zq7}A{tQe<5Do5p<^yg^imh}0mh7y*_vezEfqVNFVj$aaWDqIRlj69nojk`Bve5Wsk zftQNKv5slQ2xfOki6B;)+0p@Yl8J%XIxe~1Kk-C=$}+TYD}^@(i8$b3!hra@V#a~fUecTb-<2D24U1*f{sL+9Q4*;$Wz z>)(v_li6uIwJW5bch$eCWIIl6t+07L7EI`Xc>WXY8mt#eQR0-oV66xrBHi0tMNWR7 z6qWH=r1V%D$&`!+A$yvKXo-WzzGMBe?G*1j`pP=9Pefcla|#bLr%y-y6#@e-gStW` z=~H_3w(Y|*Sbw4#tF3Ti5@P*8Ps>lk@!GmnnBRc^9g4Jfc9Rt{oqt+fK;-p> zs-00*lxfRNq2J$0sCKcJ8ZoduleN8CIJ-#qAO|5dvi@Y+Qu`)XtK&x0?x_Q*BC0HU zgZ3r0{LA|`)v#+CCP4zPvYXd%btN`H>-4>1-Q_6w zv|B-jLSN*84YA?lR~EeMy!04HNc|fDD78z@6yg_O8-lUJiaEl$Qpw%JhwC~%p z{rgM=R32KJcXI9u46_z-lTE~B^8~n-BjhKYeUojQjO&~q`ZXsVc1EW}RH1_Auy-%{ zpHDxfDc-&15d41AHvRZ`;d!=ng4-h&#j-X&7s;|#$^A(lfb> zpaC;_)fU^BK}q$J3zb>?BO%`RS5L%j@@3_CM@ZL5BQ+F9x5zt`_$jsmF4C%1poBK+ z^!Ro`dS9^m8Re`3FQPOs^95;8rCO2h-e4|5CkW@)zDQvoFLShKX+)KCoNt(AA$dwD zK7nO@iws7H_AtrEt;65NICYH5T30pL+eJ=wluVgr57}`d>R?{yh(Oy>$DBDVW-=6% z`PiRAkgW8CzW69c^$JD zM}nf6tHnH%_1mNfOz0u3w0+i}?127rnXMP8;t>pKEGsTp3VT$bx>o0;JsPp8l#CzT* zPa(Y|9vfs*@7ng~s}_8E*KBg1mqjY{UvB(h+quJlJ^rSO$1-HxK=>)2h;A~~?5Fkz zTug7x1$6@I7VgnZVn&RKjk@lu4!40Bvmgy|<{joD-0 zRqbh`v;J1wFJ;Z5l=(DDbJ1@TU()D2>ml^gpb!0ApX=)*i|eH%wzc(8Z}_KCM6gtwyp_>wy#-p{@$2E8N0 ziHag*z0bZ!BE`kev!fgIgdUByvoG%hov7$j;veEm?^(=46nd$vbAlA4Z~98f8D1-g za?2r_u=I}D@yz7Jj{RtQu2O#Xm#PSO$?Xl zIXX38OpQs~z$gn_jg0-L04KA-#vzpPH4YVlMAQBUrVh=qim;Tob@q~I!LC^NF(@Vm zqaeZ@FxJWs44_{xCwtg5WUtxk{wT1v8iV)iGnWYONf6$Dc}zqd`^1|kMIqMTBtr0- z``j%5{Q=grBeCN~X0U^M3Id5ElW@GBvZtGY{?TEsHe0o;RSpv*!94Xy7WX#K9o4)0 zsJjg@t+`i+J3HiyR6Qs_606#OOkQLC-maVCD6zYp0wJ%lxj9mC?VE6H1nKx=jYQ=B zjij~MVCq9DBcp2RJn|tzA|A&ITD#`w0h;8Cptz%DZBR;QIZMlHrLQ?5{*ayty4d@D zY;~i?qrbw)Q78WYcMeh6Ca!OUgMe6Zy@2tq? zA|KMaSv-?VWbs=fYnAzA`jgT>_97D4ujdvwRtBEh(K9mOr>oi@bGXTo;#r5XDT+jA zl1wlMqY;QbCQB-z(Uz7w{SmnJN@X0GUhHOkkgQOVV{M`@5CqC^?>!6{u6^wW;?zOj zZ3Yv5)9PO#P(3^;r!}X+EWW6aLv2(V{r(sv$J+M3Q~<&s%d8E|cDVk*@$zckHgmDD zpN(H`8)3l1?)#W;G%>#z?e!G4dvoUFt5_ax8>zwnvG*YHq4atz5FdmbM9$k;n#1BN z1V?y|PpbF7_}*N<2zfu_2Q?}OdK2R=?hzcM1uJxlW3ht6JvBY=_69{SXk8w+bcTW& zTs&(e|0z}8)x92HeEs?D5P)o$ZX4Bie&`!rSX8B}lnYH+$9g?eAa~M+n0ijITzA`a1=E`Y zjbV5Zz71QsTJqbI{oN)8UX#A3b z`L(SzmLyUIt3|&5!!M8`KJ&`5eb+p3L-z;xqkW^ykw1Pwzq)lh^Ed#AG({GQc*gq9 z^f!mX!t}KDfO1A&;rz}Y*{dJwy+;dg4G;Pcs16yy1BLt;9){z#l7xvC6M^Z)C@ z|IGr}5dXIq;70!cp9Q~M4xU+H;1fp96MsqxKt(fZ*7@b8=gzH>ap1BP_j{k`#m1FP~L z$MfQ-U9tRmn}6~}zwpfRzbK4ZxM>0Gxs$A8jA7I#}M?O5w`uYIbOZ=Koqx2&Vj zlCEc#_-=_&i2lUU+>snGkUI4nWfQ~`?b(GN_;!hduD(Nnhy+s48ERXFkO|)os!h+5 zutkVI!A|H+_rcr<`9JMa89oOp)9(P7UI6OC=bMCpZUAT*oD_0P^Sby}J zr)~!Rkji_CU;A7Ft6hR75Gjz0i^DBuGL8EVG(Qx_6u|wsJJlOh*^im%ipN{NcQI7q zDOA3YzKYs=Ti>5U;&5PkC>o>~o`AvN0~+A!d-X>>^tM3!$FZ3rcNtQYgR%95-bQi0 zvbsbodHF)TGO;mF!yZEI&*gn~{HchleK!ajRQ8id!2%v6zP#w@SOFdWDpC*-siH>LV@c z`{?Z`Pup_`7}FISN~&Llk2=b7uEI;L-roD5ODf`)3W|ns)U*#k>z^H^7q$Fa&^d$V*bP+c-f-wKX-Q#ZnTbW&2*MB^tlZ|Im8CTpd1Jl{?#{ z6rQ9##(yTqX`SQ|o+72YmB?Nk2hYwrF0NCu4?tMZ=rmL>L@wj4|B$)m-tY>%f z8@*eRFa9h2<}%o<=MDU)sXA9feFuPJXF*W|plAe7XC2$-u1;q5LESPV?HRoYFDSDB zT*=j!&uPz}Kj%GYy5EWzxmGWsrn>?pROj%*^GeRiH}`q<=URb83eJx~#9Fa=7{*&JbgvYb<^01dXCC?evEWl}wQ%DSAJY*Pzl zwl`-h7(vwmU4S`B4+P`|{S78LQY=ti=W-Q2&;t{_*+Oq!?bQq02o*gBK=b<*EwJjK zYf}Yk#U41i%(WZj==DM0IDQ0TTvW?GwUaV%D=aH+Ab_h`EU25}x;VzwDKaC#qat3? z+S`F@^VLGny_$8PD{}*{0-(Kd0}R!-fX$&xPoVE(QCLIQ=_u14Flza6u+$1D*v=eP zk3?SQ=Uhi@ldHiUn+CMD{6Bw)Xn!$|^z0;Z{c#L9-9~ka$B0B-^DtSec91bXyDqnS7G$i zRc60i+oF?iW-cqidjTW`&yO8$nbM|c+%up7MM4Ny7XN_%N~Rtts9OcH!{X{a0f7`l zZIaTbX`1E^0~&0gfE`V>(9+qIa)7YIYe~xWo#T&_OhiQ-mMN>TTq638z&y#nF}{?)^ac#Lc@wKceLd-H z@T63glZ2W+0y_Usg=LPVxH%Lp`3=tB90&8)K90Z_-c;7Tg8RbH_e4K{O4+?GiU`U( zPckJCAm$+jWnd>T-;FwGcJM(LI9n_s(6%rZ5--_aXcVOAng8y2{Pq)Ic;8R<dK|!x{e;B3df`e?|^uDH21Y7lrA_gHg^`F2}oJtuUpTeIwtHpXcUd1S`j5=k+N+#@t{uuirB9ZW6w}Up~ytx6wUMrLat*Rr-Imb zh^Foh-grR}wM{`sAkbpO4py09@SHDYLqBdY#1PTB) z5v5RvlE~Pf@B%X8l-|YB&NqF7qgFv*S95{w+6MhZ&BlX@L$^x^!c%Hy5=Kve?MTh1 zFv9mVoMlpEb${T~W$*YFPAs52@dT;bW^mXpc#R3Q9!ag#IWu+R;CtCr-O<{NY*15% zRMz8@%42)oGFd%b8sgZ4OrKOqXQiP!ZnZPoIADHxTU!sWt{F2hngN8638AB!Le95n zmL7Ifv3wcHy`ij5u#RrvbqIjJ^d`J!>tzs8{=T|TLTzoYUtL|DxJH+Sih@)OUKeqn z@A1OhNnD)~c)di8@a1mR0%oU38)cq7Rbt4S?5-(~_cae>D}@`9KQsUwF@{E-`g(Xh zPEEJ_r}cCU7EOgLfu;j;vnb1_b@09qA*RI)e{_RO!p8fc){H4@0cgv@aGDvBRk-Q*!uu!3mMjUN+KCf7r^0?vC1ALIFCHU4uY;0 zpMn$P>=AuoIp+P<$ClB-d9x2>szw*ytV*;Q3A3|Cb?gAh=Ojf^^TGIIEhThCdNg-v za2}z~`^=O(z>NoONCwAstexF|&_-qvWE*RHTg*ucuP?{-b2lMfk+%`(xg+)C!@Mys zW|o4_+6k2bS%ONdRyZcK2U{0w@R>UG4a?irE&HRRl7NatmiBNOpSAW_Iwhc_C8n0&L6@LJ-UFYzP{! z(}vJV%E-qv?~ENapufRGg~TO8h#y_{GSyj4ijIbhz=pZF9>%Z(RthT?i>&E53G3rN z5XA07Jo?c~3iK|C>67)i9d)fP!`&}1)D_fAGOX^du|GZZxcur4fvNOqsk3-QuW_(> zA$Y^9xAXSz^)*?Hf7O`ml}qJS!KVj`O^EBBHVj+b-`e48UmQ9=i=+i0#%QSeBjZ?b zpXQX#p&dfGBM@?R`iB#z77fX3{kc4%oh}Myn+lNGmRHff6B{}#RvEvX|TiaO{X`_q) zuhCdd-TRA}>Jyop@EJ)lnzrq;ft40Zzom+Xo5rPU_*!i~M0_j?-3VsG&158t+h~d< zeUv|&oZR5wIFB^Q@_zcU#+(kHyUUF3tA140yOO0}riw$SiWmn3Kt#JLcgXm%d^sY;(c@dv?Gum>((Rm*ej z1a$hKq@gy&N2}Vr@JZ63s&5|`xlvw6luopLC9NA=tD{dA{1l(%Ftn4N70PQBE5n|( z5in=C+P7+KGqDV#i|z_{6Y(!0yEc|YT-WFM^1#ITRYd(8T>>pK|8y_y%q!J`wnrz4 zzvipelb?PQH0hw0v*oEWw;FGWH~c(6T-tLf3Bg6(QXD!dmbCiwJ~DP?zx2R zI2L8}&Ou`xnC_5iY>m57x7isg>hxcq zUG*AU4{#<1_2J$Bd`}w*W9dbvy?R#-A`O+k4rc!g^^gg|Cp3@Atck4I^-rr$L(h%t zdzT_TOi~DmO`p`Rz2@ zM!GwsLpr6A6wX}u-0#`@JU-*yANPm*3&VjI>so8hxn}(8UkRNuU12%e<@P9+SqwuY zJ}3w{=CfT)sOTr9xE6F1tEhDnwlXe7Zx7`LPzpNqO3b1si1V!!AH4PcsM#@R3N%A+ zeChv68>acne%zFO&^s*9AnCfcTQK8B*I#qQYMCnWK~iAu7rV5Oy20$YkPyzkJqbJ!Kd;BKc&g)|PJ%FQS`qP@}& z6s9$s$qgS?sr`6g5O8{K0reo&AjUBtC;ULRpbMrxTSp@8Lyq0m04Al*8vAyT26IHwr5qqIzr?c8jBSHj(S%RXlTMrhi6aUnA-$AHj`aKWO$6CF#I_IWohb@Bd0&mYZ;xs^pV z)?R3LtIAW;-?n_oiI!(Ucm#s%o-pueGfK>@D&gyMd}aHk>-!KKD`Kd5!G5aLtkdP1Kpn z+F7a_M8aohc)f>FSc^HR`%1|+@xcGDv)h1G!GWn~6ULX0e7MeW)ct+y=B9|Yn6>;S ze8A_*uO;i!-2?ybO_#i?jHD9u5bPk@n1B7^A1F5=7KZfkT755=;iQ_D=5 z*%FpU&zp2*-+X)_xk>ejgb-`*!l)VOG9rT%I#W03@`M(g3^3ZP|COh5s^LQ*1feVy zZ^Gtlx?(L*!-;q!7(X)6Hp+K@UxL+5T-_s(O6%WPqxV+$i{b^_C;Fw~AQ~atz+h=u zX$rjeS4c*o=3B;yjqE6)#VV4?YDCCrfEDgl#1pUm6;#T3FJem$St)o%=$$V*0S`to zj+Y|8#0SOo$ON~bUPt#@!hwVM47>9=VqGiU!RtqC0p>psA*TTu8XTglvx=~gO`%a_ zU2)7Kq+k;sMY0VBUW@xeNhL}@scd2e6Ka+80|me5-9bWx;i8`KUld4J%4`#ZH3CZl zCI#AE#_kR%Ksm!cPX0ptdc9`o?iUz_N@(@H-{$%S6_$(%*@Af-ITwm6lLq5#Kgyv5 z%(Lc7}Mr3Ev$l3Tppi|De>0=o_~j9h;pEkHcNYQn#5cFdgpN zEU@UC?4A5O!={7^Zkr)MKteA%qipt$Pb!cEWKVFS(1*wjORF?AxgC#(n7-E##H5nh zf;j%oCyB8}^;@^k^No?>L6^)Hui!a*sMVFB3$^}0=T>fVB&mYdtY$cX-6nBRy!Kjo zDSTlnE*a-u#<>>&{tE&x*&3usGnOi!c*o4UfxkEGLBbag&yv5<*}#R)3?QvUIc5+< zY>)y8_t1&;a1HmO+n`Nmft$#`(s%xX379J)s z$8tRf-v@s2Z$CsA5dBh~04N|%EA!RjSFY|nQAwlqh&$Qbzait$Bl$UuG0vGB(q0bbWlOUypx(g^KV!N~_a3U1;kK{)P;l!s;`3VTP0W&`$t&=I74|C67D ztO*v7Z<8B8M((z(|Mr|dF#I@xeY*?2yt}1;`*9=>$Tu&~1(M=FG!xWxk?LSf{BsNc zw8B4Q;a~H}|KN>Co>;XDL?hLY`KAZ?b-+~Mfy)-Qew@F57mbEEt_xUvNAZ80w|{FXbCE#ju+`$_#5T9};*DG9($b&jBT*FCBi^kB%aQDmkPyOQ z?(}C_GJJ{b>WB-MpL4}!V@k1f!qfRIDnIzY5F)LX{!^?$CJ-EeYaoA;-8kQ$SPs$L znhR0x!gHbve$Apam&Z5p+0AAm`z?)sXj3}=m5Ee`dmmmEQwWasOUnFP>6t%!0a0`_ z_eiOxa-`9E1x?K%@jZZVqRR$=i=+;qfB7Da$6_Sqo61x^-*!wGSUj@^x=CjnPCZ8) zei*0C4E`9W(NGhzSQ~`<;x*~9TJsZ6WYN6cMagaOy7K7H4#*?D%uCq~;0$>K7y_k( zD-i4gLY8iUjLUq#4uG{F+ei57XXR9M(%_B6SmVP}5I;rtN=J99Sn==|rB1)Wcu@^O zOiRk+Yd6)&=w;^)IOdSvhs*|{*UpeV>JKYHxC-=*z>57HNT)AOCkM1r7q+ z7)73*fEOTSl&c=#T9>Cg35r)UFJt)UuR+R_4WOXF_IwA#26KJ{CX&vhBL@xsQxJ$N zu$9}t!&2%3;O}aX%A=U^xN%*lz3OioY)A`VXS_03^2^T-Nkzy#<-)?EudDeur=h5( zgFc<&U?D$i?9cG%bKp=ws2V6FUF?E~>lLABhxXJ^>P^silrB^Ix@U(lCEVV-%txeqU!Q_$ZLe`I=&9NVsEy3UkyYY0dv(O&{RzM_H{>u zMXn&#C(7vtxahnAQ}8 zhx=7Gl&)P>B1`@)_2YM4wqhIAFN5Xp=L{E?i2CJuT>fv^_5HgM6H9*j;c0~agDZnrwB0CGB3}SMB0Ctn;<*CT+1scH;#ASkfN~LqQ zWNz!u_6^9TT{Ua_=JwYaILhF1pM!?j^gcgWa^kj~s{wor&&4|^GYcfxEC56@jAUvL zFd|rmlHfYCObyE+j9~hKdj2J#nTt|vo!JPE0q6Ao8^&`ot9VByLXIVI8 z0^1Gs#RRaku`^|9!>Px)DL&>k+5wGVT!EXUHTo4;GR!o^m{HRSrar3EdI5){HIy?A zEI=H30^7c(hafUYB2RD^8LObjdJ$m%g22Cbez@|HOL62qF0(cpLh{yV5e`QQf?)F( zFn-+tEd^pGa6TAobzfV0wMOF^ctn}*0TMuI1GLRgNMrf+5s0tKZAhvg=(psNNOuSH z32?NLcl`o*1G`{~?gfEG>XQ{f4F=m>fur3MsT3!nD-BIenIS+y5_5LRR>s7wH*fs_ z_M{N!S2x!eQ0TnX8Av;h>AwLQS5YIZOSFe#Wkja>$GEkx>ayDaHGHxfqipWzd3i#> z??6>qUi+Y#CK`>Ocygc9SUn%_HNDkDZnkl2QCVy2TQj+kt|10%4SZ`g)w#Tq>DB4b zPNlSJFQ56)s9@a* zAU&!A4}b;y$>v0ubNx@uN7*loN5`0`#*)hz;0P=T)fxHM>Wez&j7BFK`a(gHR!QJ4 zGJ$^CHq-96Pm_RdRNhXS9&mL@wkG5CYe=ywEYiez zbdv{0=DlCM$1_sv2pP!y2o{MSUF-q6pQoSweR5QBo_JL^vJ-CfejaQun3DUg3(XzR zfzMOSBV%MI(&=XTOmq6WHR;%R#iD=KyZLSFTR5}vIhOy&tzw=!=IN&Q`M*yvf1i)M z5g;$=&N1H;^ZDokv!5(IDx>vGroG1?OiuI%d|4Z4vn(}74>y!NqWlV4YlTC{Y!Sf)S1O_}qtJ`WoV%ZQcfVo&oV-lv5hP{C?35!rUO(4#KIlw+G@S;mPwxaqgqQ&FB~w^?}BjkSTWqJIq8C%)JTj7FKUZ{hV_G;ldUm} zq4WW3xtGH@R@O%&hYtbFhs|KL$FqNQAgVS56(E2-8D>QvS&o2y!xw!bTlw=nwlyKW zz{5Vb0h9ZJ@$JsnpZcc$4T+{jxfyDEfHiUS9NNeJ+rIP;%>W97%^+;r*KA_sNOD4f zpL+tM@$&c{G(85e7o}8K_k*-bI?*TFNu2;ar12+g!qqZ)A+3^^B!(+w@&3f#pDOd-$1GX1M4xaqn}$d*$yP>J|bK-)#5eFLcmosN}3)9u5wB} zDB0=hP$nJcg%gbdCkp?nW6tZX)X9GOb-fU+<76A|9(%POU>LDPp9!24QjN5@1OjRQ zU4|~nj9sA*1^lM#a^_m}6}n6n<=_+!kO_r8wra@Qr>Dt$lv4^1V*s4|u` z=%k<+2ADOA@6EokXsEMG{H~CIEAz>_WZqXw5vO2VLGvob8Smx{WgFGNt_3cOfeQhO zL{+Py)V2oo8?vxDs8i_Io_O&X!Mw8UjJZr?RMdJ8jDiVuxS*_o!29tPW;u^Y&-EU^ z@qx|xdfpz(=yKEQl?{xpeUek*#9amEP{I9%m}i##MHUV_S;DtcteT;dvG+h2gGJ(Z z1(tItCeh&pKpl!N_!?4%T|w5Ip#}#h$iYK?gq0!A=`ct9_;q$tyW+D=d6rr#ud}I# z<`EzVaj-#t!~(NdNRAzQEt4S0>1agAiQRpV;-j08o5~V~G%@?D1tFf6)y;w;>Fbuf zH^Mc%A2*WvDwNK?{?=#x`vCLBhJjnPQ)e1{Cn*&Be*9$`v84=gxz$le|2_?*wFe6K zRw-x&%yXDrLanj-^B65)y!S$+Wb01Z=nE+ueU1=-u+f&e-|4dx(P{ZTAfoqq;knTS zHvV9z^M1Jx#4C^;4lwPe3%{A@NtX!375Pv%@qQ9gw*ToB_yT(*0N}YEjSw#t?{~&{ zHfb9aM;>A!k>lWx4NmIj-SvOnuDASgIvL&s0_w>YX^*Kzk&%EMp$(l4L6wIYzA679 z@njBu_0?#S9QA9RaSC2i%h&EGk~)RudA}1k{;==AeNN{6U(mF^<1erT7Ia6%Ep7iH zEO-X9dolM|&ii&eKIhjTF8KR!L8emh_RpJl=uaxCRemK6g5bs%FMs&`=Zmar8v-AH z?E>tt=b(;6FBBk&IVH&pi5~y>#VX>Es(rE~km-4KgVbG#fIoBx)I|ll0M}ZRs`_Jp zK_BQ=10VR`a)s}=r7nwys?WAox@gJ${A8kxTizOX1t2jvGZko-81FY0TkE}H{q(u< ztnxFQUI&ZEvt68)&vcq9u#)umFv-Z9-lu!cFYZ<#%hF(RWL&pcJ8Xbi-g78&BYh*? z5ybQVxiyI4$PeHHOj%5S3NF3dH>mt`g@4-Nzl^Q_9pgb%_Lt;n0=|Hg zFQHbzJK+{xf|+yrXbNaX#v)5)guk( z-dK7NA5mCbJfMT5-RT<|8Vcl{lf^pKtLxVS<)3vVK?Rm0LEAf;M&)}GOy}5NQRL|w z8X7ER-4qX=-VWWhC6{P3pU$h)%EcL<{kVGQR~I;zYqA^QnC}f`C$ZWi@n3>k6Aw@b zV}}zbJHH8E8Ff{@dA2uJ?awu4es_%uiF+$nCm(ae@n5MY{ zB;%P4QwNAGepFZO?tdwUqDRz_Dvoj;6%`fr^z^pXSxbsls8SCnn{dOg%d+JqQ|x1K zFKkAOz9pa+Hh;xRhbEQCN}ofnY`KVhu+hE&N@#-h{!l1ud~<}-`Z?0?7CQ*^vFEuY zq3Y7QD!!qGo*Y-rS0mkNU7dZ4`1V_>0bYtg{oXqSNj(PRdB$}YruVOH@jeyNzfc#t z9g{81$e(;!qHBLOs#*H!B~$Kg)=P+AisZbvvUS@&c5w{0y}y7uF3Ng6-}CC{+S(eB z0vr7b2clQ2%abjD1Brnn#s#+pX%NmG^-{rbZ)Nwk9o$Uu_Uw-yKu7r^g2z6-7V$ix`oo8ZQQ}>5>k-_6bqOMh0BNwL0eVn_ausSwG|akAfIGw zs+3Z8BZ<=jlDL9haw5nI0LPzE0R4eOHRqbe5>zHg)OUi`F!g2h(72a%44XCt6Afd+ z5(@sc0i^Yitp4UGp^2%q9TRN*Af4|GnD@EQ+id|r0~FD@E%oIl4k&Z%KWXb;E;Lyb zH565;yB&@`>f0xM#*?4-(oN^Vy96Pwx(aDYw~eFhveB8FKJIlaJA=Egwt)j7Cy9@o zr?M7CAwe^M2vlFOBNmnQG62D+9$gYKdhiG&UE?{;Winwyfw`9t%6FNWnNcN!hq(r* z92;Ju{Q$P1>L5UwOVbPpc2m_x!|&cTMaBaV<>3PIiNK{^&b+{@;90SW(Pe;jzA8q# zJS~T+5)8w@5ym;0FSXvg|3C=fPTp6ui$JBD1Z|&zz+d379V;fmsCP2S0{%^9WhH>! zjzOU~nYz@J(UVcxguo*xsmLPAXmQV);k(P;Ekh+c1fLc$R1=QMqtXZjlA+x!L@30-&o8q2U|ASkd0#SHR4ErEJ`+5oc} zZA@`{vyOiP3R^)T7Es}Y2S-Ur``rX$q(Fxj$}-q!QrCu-D}3PxF|aiW zwII}sJ#ZaBaA`m=e$Nt2I8HdUipk?PEnLUGHT`7Fk+lju`^&GIDvb=p&LCD-J61F& zdJxPTg!6?^5*OIr?@oRpeI)Nt*}wgAEDweD0%#ww2nn7_vN{3e?*>58ZAm~|9|x)t zHUKnqQro&1pu`cLJq1S-O}D@H!CB)BNU`-!hB81(RZ2Lgc|RXW>;(n5)y$hUz!a{ejo!xwjN+OgP ziOn4#(v2K$jEb%&DUc2-gekg)J~qsF>dGK2nRrH&UW|7v7Ol+7u~ojM0FW2yNvRB_L(6U)9> z^fr~43y%OCEvG<0N+(5J4|i_;dx00@1rYwFv#ij%J)CvlnggWf-012=&Y(VtXqj2f z)C!OKDFnooS?K^)ehqqQgU}3A!dl-nqI`}F&4^m3fU|)@de)MCexz=};psMi)wP%d z!2}?Sj${(o#^RT>x!N(~EWiKix-cj?__UiERb&Z$70&X(d#XABWEHSo+l6Cx8poXE zzX9|2PC1^N{LQ3oV5pr+7??AFko#d zCH=VPxY`}XwAlX)F()PX>Uwy<+z3S~wGkly4JiY(W(0SWzZ4?de!LPfqi{4gRE=X} zAO(+ss;@s<+UfGVTsbdsm30Ttsx06I zP8hd@IKte4JA=0HbJfg?ezx`D)vw|*6w7uL4fSC5@bi(eO|E>rS&g~B`EArOpX!mL z`I*C>Q);lMs!HB{`8S(OWk`;3n@;LA3%2XPuBC(~;1mu8IfGR>xpC_3_GbUq6$5o( zw-_l(v@K@vq^!H&VTW&R`YUj3P&xN>j>=Q{ zV5}olQ+*USsAQd4-l8RxWTggV2TrebwJWDV! zaQZN(j!&@et5fmxIoM)}fW={Mj^_EGKO7zfvd=Q2&m32eOW6!`7hLIkdH72x$4yHNX-j_WK?D)5mbnHlT;_^_dnH&ED(-Z?O3QWer`(>G&Tjzj4 zsyrl(=f2wxo8s00x~!GVjH7(+BF6%|cMOhFq;Jt9-fKC4A}3*LP)Z7ylfb`--R*_D zW)dUXepxo0YVeWMgIXO~s%0tqCQQD~Axu6s4F}+_aKxtQh>|Ah8UF_5s~MlXl#Lq0 zTc7@Gj`%}s$*Z~`@%LANUR4e%KgkJJ*X8jW?o^<(b>?t81FNM8G8U1R_q&{7Qd$*` z?&Lu}A}b!9zsM8IZg7YYdTlnGSt&okZ}wA}H(<}}fDt)Bl{eXIE%{|ZTZ<9XL+Y1Ab)IcEI$v|8-n~$=vCPfi5KC8E~jp37cip;3?15eKh zL)MYUp`$sM?XtgthBkst22aM9WT?u=*l)kLkIc;7#Z)+q&CK-%-&>Eqb1@6Cd>fu(kKH0r~dEE z`eQa;@Vylwz8Ya#=9Ito*!ubXI{N1oz#!vL3pr&=dfZWzV;6LeWmLl7Xkxo|q|xZP zL^mG5XWhhoL$qlP-hM-J96Bqz`lL*Wrl=FT9m(7xD#Q;X^&{~mn~oJ}f5ntNsKUbQ z`H|<5;3MY}AyAvs2fr3;7NkjTN!`$w6av>oo!@dqY;TbI=B6Mi>VT>2rMSnil!ijzr-1QY5~xP{@Z>Tl5=$-J?2 zkz=Bl6JDF6vp)$f6>G(c7qS^$w#ih24#%A={~&p4$m-U}(DSMoNUyHiDVnlJUj z?hSe-m@kwfSxaPuu|g;HcnVR=&>^On4YL~*6B{jqw=^6c^k(8Q_l=(x8Lo+txok9IYNiAu(nU{KZ(4mY*$}cn7p*)gEx~*3p!y^=yi38YH|sCY*O>Rhg4dt3xXP#< zi@SYRZY|3?JUxeUELF(xV35a*WW{Y6N14O7EgpH3RCYP^*ktMSa&%pnR6Tf?i9pwn zPW_mp(w`L9$ry$aN#)=yV~TK5QJ#1A>@KcBA)~x&ibkF-RC$6!x%hse%d{@m_(ZAMBdzyNBvv*>|KFt89{1J`5*mA8I~T< z&uY!TB`7nxXL-~#7xw|J=NTO1S0CjMrvJp2Z7 z0X6tP#MFUcBe_Lmj&p;1qfb9Ky6p))DWbICD3wM?u69*Sf^=|o;%tX3dwf!OgP|Kb zjH45_z6bE?GA%w?jGL_RTVTRdTSC8<4@o8A2(_?(g8rjplbNv1A5KRz9QI*d{o(7* zCM7llx6-NjMhnHysZG1foRhmob^Ji+0-+PL~$?i{9d zcc)KDL`Prg<06yF4`o`_Czxwjp`qZ2O19w()h>y|#)#SDdLv2B;l~N0(v;O77?>kE zjwM?oIMeD3SckBihupJnZ%3z=WwE=!SQK+#F-5PjF{C7T=ssBE|KNbt%F?~nNI|0l zZ>%bjQpkCCO2x;uw{&?2cv=2Zl1@IY>^8HrPTKK{B@HCHW5SYkeFm|kqMnht?zx|c zdtHEJ%J(evy=vlf}N@DX9N`maZ zm4_(}m04mF#x%}AsWQ!k+)8IdQ(Go1(eLzR19-7b$&my)kmV@`N43mj**7GIW#QAV zd(jSq_`1iCK1PxV>}Z!fFGzLC$+6S*S{r1~X>VnC#uY~6&vEL^DZ4W^^noXxM*>AP z)yyTEli!ksN9IgA@KZHwD*52dtAUZ~kr$BUTo;MUBbSpo$3pT&ob(2}bX zuMt+-pXmM|}LW2rLq=KN-hpT3$*Q+X7nvZ&q**Irmr5 z`^HXn-^Iq)VSH>k-%}Yg;S88~u=iO@t*yAMp2Bpfr@s`PTcu%7f|;l%n^YJ|m8+5Y zzQA1F7@Wv-(l5Mqo@4i@^_|-mn&X?q@^yMCq0q$H#)dq6tp|Jin4uR4G^PYK$cBj6 z(OLK-JlZsjDMRT|x)Rst!3NX2|2m6y!h=c5*Jn2MFm5=B!mh;q!HSor@VhY1C%&Jt z+)OFpDbu#ZRegLqlKtApO6XwE_z2?Vr4oOfjMVVSp801o8GR~az zp5Tp7iy<$tUnEJk2fcDQbj#%#T*p`x2l;EhNPf0>J9MVbh6>6-kCH+UZF8gqCUxE{ zAVnTfzvc1fbiIaoWP%%N>dg7*wJnF^I{{Ya;6dS=bm!xDt`cB27WMVlCNnsU#jp_a z^nEhm)S-3t7LGS*%tT19ph=!bAFMKTcK0--O{ZAz`)c_|%knG*dt8K+d*C8hnwOz| zQeuDi6YL>Y?Vof`!`z0rUqTP2j!!=@HT=b*+l1O22ZBtkIK!{r7pj%!(n^n&1 z{le45=R0?7Nq6X9PpDY!b3xb&*i1<>c?IQyz*(Nygm`@A-3Wf%fc2h&+&h>C{@;6+3xd`4M`Mc$i|Lt^OBUZGmVTNnTnG6%)p~?Gi(%w#6Hp zZg)QRRi$QELYwgX+N%gHD2qs?8@hrGx%aKG3ka?*hLHrZOD~Hz)_;He?tA~AAEvnQ zjsn0B9#g9cfJnN?fmY0pB*y=sSH9aIm*H6gYc8bdxnYa{zaAL7@1p<312fD9JTPr@ zbN|f)1DLivGw}fAp4V4J2GEWK@ETNXpc2ET6LL=RR?MmH-4`TFH_OZj4U$MgAIYpl zY?mHQP0#7GUX7jX{j>B!)VCGn%js%nR9pQ2+8QjtC$arKa(Lo^Q!Wq>LrEVrqd| z_s@SwrsFHc3=-;|Xe-o01#bzt3#+QCIx6TyP?i=Xo4kGjcSF3#xq-#R>rZqH402vw z%y&0jEYWO<|5zr+i19+~4Iy-wElBv&JK?)=adDwcWkHYT-`+M+fcxkABx*#R2Fdu+ zoM%32`RZ)91fUS{SPp0UO*Ha+-kU}f{5=23A>%7^<$TrMyqGxxBh?l(Y&o_0DhB6n zUS2h5?oQ_~gEF34EI=tWkK3DH$G29~FwjhB2#d(LrK_&4{`~oKHd5O>EPslnA78wg z2`|hf6GC`aXqbX1FMkmt*D0mBBN}2B(JP6I=r{g;*2h$88 zH%P%a$Y!ChkjhsIIB74(f6cy`CDDV4gkdX9n#ViF>K2opY*_{Jl0|Pit|Qh;2Y(pH zaZA1o+3*&i|9N~;hZ4_dYe2~8Tp7N8vA?MbF5$b5$wo+slCo33ZvXp=;?3PRB2k_< z*VE+|Q2zdE@`V+EtlZo#Qu!SZqTEEl( zK-f?dK;A#=)$@EmULFaV7Sb!d$Ia?g4;z^9_46Aeg|5#(LTBQiWmI&e+NPPiu>9a^ zdNIK;ANtv|(n;yrg?z>Y%BHaA?6-aaR;P>-6li%KMZ`y;B+N0Ic@_K$YJQ~JW`I33 z&O6Wk0w6nJuo3d3mx^ilw}1dpAC#Fvs{3rx_7f3>r%akuH)t2&zH6H!IkLP7@$t6h z13Vz(iE2v-NDn#yYuFxy;kYqKT{`@m$uF0EY|nVNhmoOINp-M}CwdP^bxAzE69DOk z_F^?nw>HG}bMJ!MloqM>QtKQ`^M283f-aD#c(&3yVc;u$pWQ=Hn2E>rHof$yY({CV zL*9j{$@}SaYI4=uO9Ev5qv6d_5}dWPzR+Gd>nqj-%ztt0;W(Cd_%C;TZ&S}!-j9?V z8c>|`-*GKXz zDbb23eN5&31H6mVDIciIBX@jeX0)C8*J9Oy)xEv{ zif6VSSUV_v9cL=6!bK<^KbpaLO_w29>^#w+srWvmOg}U}A--K>S8buO!Nbe`$H`3L zQ5m7yP*3!4WsN1KuQ_T?)<6;^{JVFkpeB;LaVT>YKBbS6@aZ@xXkXR`LVVz%?5^I1 zH|34>p!|yvi2Vu|A*PXPqq*SFfm9ZetiGTF&Vc*m7vT4ytUnr?Hc*9|P-7NYiXdSWMDuy5rorpG|TsVTu$r z0onW(Zt%On!IOSe06BPgQxi0egF%V=V14Un&u}{Mr?@@N4}h&Cj(z0%fNC0))_DY~ zwx8G}fZA4ojo$}cFnlE-)o_vPuR%GaEYaReH)~)bGp9P0rA=$Mf39T~2QjdQZO%6+ zobRJ=J-_=t+%$=C%2SHphssm!gR}&Ys+kl`?nk>HoxrRBkNEzHM3-fs z+8riAN4akeJRL>r{fL8-P0zobY(G9O2rqm;RUoiNyYpL^LIWZ!q{iT+BjghWLG5ZK z;Ff^QWF0uqscg#>$g5?NlIlANt)Vcwh#4M}PUP$*$h)973nJqn<6A=Bvc@JS%L9LO{^1Gz_4#^d#gcp?OtF5(g#J!+Q$ysYh$BgH_Jgsf)l_G zRewKufx@s_bEG9#P=B~V2Co<43rI6(Vdp&w0{yag-st*Lq`Ku$IAm9M=s@*I>wvz* znNtLKoS;MPDNv-?0B4BX^{BfgG((HR5~etv_a84s;w1huKYpnnR?CooI%19)e}(kk>AXgXf-0dKy_e9+JRnSxdf>G!9rzS zk2{t;;9o5H{#gDHAm9hS$xclHg#gQA&HH|ixtO1+rUrwykXUMbJZqP0f$BiE2&ye^ zWAP{&BsPW_Ooz6NgS8If1?`AhSyZCcHiB^gv(YV}OX;_x;u>`m}uJ{M(- zyv0w?18>`~5DY=9W!-mtLaE{1;jA{`Vv|?80700upfzjluYjrv@-NY*+JXAPSYo9B zDcZQ&Z=%hHyi-M1H6U02NF!iaw1VO(`&5WvZy16$F!%-KkISm1G#11pD%q{hLjl=- z%noDy@HB*&(Lr%ci84L6*Bc^5xjn*G8WmJxQ50woshMm+UTM3cvyl{rVlR#BFQA42 zU*rv6199${!2GaiB zl1PJ>xzQc|Os$!z{~YLBXpxokzR0mrz+=|Zi4L;A5Us23R>TsJm3>btlHN*BNl(xZ z7NM8G$f)|VK_A%XAoa~TmI7Yy#~6E6iO<87k5?nbm(N}#{nG6;88LwDaJ4uPh#jD0 zUw-NsyfUY4_yn`AZcfB?E$;bt_^xhgi3cYsbxw*Etq4__mgYNt9ViTMpXcKF2;$wL z3h2pFobm+R-y@HMoQP1&!u0^=V>_q-b2e)=0c-6lrhE4ZQH;oasi*9ZLzO6d#`K)0 z{WIl}y<4UT(6DiD913dX=3rM<;ynxHRiOWYf$V<%IwRv#XPFwBg{DHIi58cCg|x|JTPf1(*EWcbXu zr8)Ptu<%C>y*K>%UFFtDN?#`eY>D0Xghq%kIU&bIY?7$cl-OU(KiTxS1y+SH(gkyDOlhzhe*qJM zeGOC>&1ljlSJ;O9sW4En&HXZ?R8OB)vWUB?^qU86owxNRumd)4C&3Ecxn;+)+x;2_ zC!o_0i~KPriWO!c<%#2Qq79#mH^|6ijknLeryGe*FqX#6O@!ijGGC<)%}4~W__q?P z%c65;R3ExYug~9c>;}L2aI))KMk>tr*6_r(uG-aS7?e4aLkmr*-=d(8L$`-lAZH)= zGT#4V%I|o9xOZ&HbWeJtG(%p7dcWU}<+G?*e$Dh{?MY>AYnF9C4yp@A4TsUSMnJdH ze!9K(+~a&?R?W2NdJHD)P8^tBrp^A6a=^lK&r5%IhPLLuG#Hj|b2)$Tfz;e!Vc7X| zo{>gVKMe29HYtvhIXzP+wO(ffpt!;|kHzH->sBv!uv0+ zBGyJ`y9`KuzMrVoiG%s2C-@lS0L#GQM8IiMO(aIa#Kb8Wsq$)yYt06kK7_lwYP4Csg@{!M@+S^`Y$nWh_`(z3I&?xQW zaJ#NRc0&iVKCz6zah-eu%PJfu1=DOB*cz%D=(<4r;v;5Id%ZrZKRt{5dTj&k@k;74 z(YG%KO$UrF%3En2te2^K%EtbSIK-F_OjIHQh({1W0pJ6NN=DDJbw~)5|Zt99f}UBm)Xg-(XqH+Qc+~)p$#@Q zm?IHb*j8{7GX0nFa{fxHChGN}OGcEmf{Fkr;kojO;(&HMwZR{zsIeo>LQ3fL$83V% z7Eq=Ql{82-7R!BRo)IJD^>SH8Z^HByLzgQ^dWeMXSED8Oh3_JBw^ew%#H1kODQgo) zbJA9uA0tn|szX7>25~^EQ=ho(-bW5ZCY#(LQbe{dfhP7kkH=+^YOmxhsf->~`g-xb zOGxtSP?ezowW;I$?yH8u3b#a~dQU0Yrrq!)WQb0-k(hKx z6ZgtZ%4W3UXSuv_-0{r&6b{Glg^n~siL*@>dih7Ijq|_rUSeVB+bq|=cax=Wu}Q1M>qrz8>*k| zdGx(B=*vAwxk76tBbJFEFb~4sH0fYv*vFPa_8J13wqYqrP&4LkVtvW5L%l;k7L0t| zqOhV9jY#>HbbE8wgr!;gLrzzo&vIQQeXi9~zHfI!DoS`8o7X}l;hv7FsYMd@gR1;% z(KacmF!rucj90o-eo;VFwPo$t)1ouiwysA?mAFK-sRJFbW8k?IN0ag*ZOuKYH6nwO zJ3M8iwj40fkD8q1+%PaRuRAQ>JdNHUWXBJ&eKCbe(uA&08yhCFfqZI_DNB%4qmZRD zsQz9{m~tkeRGUfiQF>IjzERgEu*-Aqgj`4fJvdCg-?xk$mlcD9WEO!e7Molm;)!}w zIs(DyDy13$gbp!IeE7~}qK5S)-`sSqVh&x^Un9wN0y0p|`_4oxxij{#y zmVwugZ8X(q7L{2=)~nV zuq!^UCoJO4@WTl8xkxBX1MTgE8VMo2W8y2p{zdaoii}L)MMsd{5{kbk=)`e0`WViZ zLc2@yhc z!8y?c3n9IxLAkL;mYgGjaisVR?G9Kymf+=1Bur^Q(!mY!V);Q;!lNUS&^@@(eJa(U z8PGjAZ)^L(Q9a{mE`&yQ0Bwz6y(0%sy{!IDE0);(8yFynW#5AMl!;!V-UXnlI3g;vO^~#Wl_^+)*HKk@F zO7>tQb>wJcEA#bR2g%m*;KX=&>)KGFw^|jfamUGTavS=Xo%Cah*GX7EuCkSd9c9^7 zP=;ES9dXe1IX`$|?+?bC)*1o4X?U%+vEZbX0!l&Spzg-gyifW*_QW!_37Qdf3 zEI}$5_A}(Co~gu_zqJ6%hjLs2*njCHB6U*Z8F7CdJfB36V`RmhEjugfi91fv^H@(7 zR^ZLQ!mNb?v$})-?(ch`qSF#VE%H5@x{>{oEtV^R5?z~HqFnJ?veXkNa zpjEg5>7Y7|1UF5Eq=MwvLAP!vULe~OBm&wKU|r@|jtg94W2Ezf3qyV-nIF#*9RPz& zLa|&j!8Q^}jT`fX!_VQe*3W#IDPOc8WU<=prz65N7e`3e(rd8!bmL?DWD^b$tOvsw zc^Q4>)Ma72R7F}^{zE~bNwMPJV?_9>@4zmuV9}Uxr#$Wx1X56{n$GlyH_c6I&TU=m zA(aA9$01u;og?Z2d?3`c6I*%h1fIA!OIkC^xuW5J-{7ym_IDORGu(u~e5 z&{kyD(iVaLp`$@hO4v~qv-ftS^oFxn>ed#7m$U_-tT6bguZRVbu$p|4K?&qQ@ZVD6 zWJnsBgB6nU^Uuaw<=q=L_v+0}0mQYZFaVNQfbHGrO z>ojoYJOP{E$|=4lBp!C@q7;k#W^n#>SlRSE#~rka zf<=et6PavZdu^;?c0NA^By-pf|qGiU{U*IgM%Zkvqx* zFOayerJ8C$t0Mm>f18nZw+@v@`4g+7iK9%aJlgVej`D&UGDALm@g86V*&Ht;J@~dt zr~%a`r;9Z3^^g=FOlCJg<~Cl(I(E=h2@OQYM>)PSJJ`H?nqpBh1L%7h&yQRYp+?<< zMP%7ZO)zD&mMOh?5aYuqpzh=Lm>em1^#kKe>vgtc3<`&py{e{P71wq{@oapHsifVVYyH5lW3qwV7W{xRKp$vd%RtNPw1p@9DPhY4D_FikT)_GmmnR6a<&YH>7 z^J6(4Ch2ZVsCk#(qc<-FkrM}t6v~|2PL3tUMAJa=H!Wl4S7%Nhc=iC+It!+u1;>h0 zzxhhmU7z3@H^9IH&t1GOybhK<@emVq@D*&43_TgLDiCz`1zM!{aHn;b?d>miH;@I! z>Mm->xBWpr^DaMo{m)gG-SuDJ`6_u|?}O#~1TuAB$N}eocb9elP!sQAIb0#fWR~#S zng^%u4C8@@RADER7-|Wu8|{1u?w}jvufg?`{i0XNqV4xI{=cHD)48EI9?wFQ7tK(zW4Goq(ZafR@UDAy3o}dco5iZ!#WgJKKY>)_{_ zmp}FIA2FQr{b^S^Szvsp@C}hOE-Upxi@TA;Sd1;=!j6Psvd1syM3RlthTLulT zom;;YlKP)p>rEwZPBY?0aemj(n5t_iEPh#i;;g!Rshs+BW<$rbT2)*GV!7>0IU{S` znez_Ka&tae-TfJY_8;LvsA*+hit4V3w>bfFYJlboFhcggTbapn2R=yHvpMDfe$-b^ zuKnuqW{f;F`9&KHM83Oyr8PtO`gLukz+SP^71H%ScdYUSdHR^IYsj8Y8j~SNH?FJ-m31jDnhvuPkwM#nPA)K#=0ac4?Tj^yTaz+TJEm*f4^Duf zRW>EvVJj-$0v;BTYP}+%i$Ic^fH24sa-f!}XW8zcaImZFW&(eodEj(TKzO$O4PNWB zp9dPn6lBN42DsuIQcyervS^h}Cu za_(Cod*8mtba-YIyB0h$8H%NAQLmP&6?R^ui9kJ8YYt|;Bsb)G0o7fW8uj!w;{BrvCmQ{&Sgno5U}TZ z$OtV0)fJI_(_NfUv@VBRz^Rn~Z7J@+t9BGiS3Csf%}Lb}9O^dGo9hrFl64L-eM+Rwc;eWC+3*X@4)B|R z&#+rFVy4V2bonh64iNjKZ+}ZmYec!Mq~sYaF0Gu6J0p-A7V){R9La8LeY5vReZb6~?*d_ZqPlTQSwMuQX*-W3 ziKL;?!HkXnw+5vIrr#>rjj;V9VP%HvZHV=0iQGJp>jbw?<8eZx^(Pk_~GaFAb%7AO)@i7 z%Ihs(lWL@0hlyws=8i6Jhx+CGHm((&>{tbHMn}TO7`|;xbVRQ;l@?+ZmU2u7`%SkN zg1AdBuUzzMn?%^#*bl(;(m7+)E)R%mlWmc2ymuo(mbZ0OSFcB`8d8(s47t=WbmR;A z=_T#&P!zC3{8ZjKF!>Q5sB>?2u)n6PF8JojI=YCGJ=}+~1Ei)C2AvJ$%OSpzjNhMes&nm*2`QIR>xi!WlB-Xy zDJdy={8u(ux7%*j)sVIZIRbKMWmhOIf}I1O-)+mEHLN$1I2oN>2Qow&enB=`!rCN| z!cXJjmnG-SmPMfupn{!Pot%i_;v!@nAWBMjv7+oYFO&vxCfiZgB@vN$8mo5);(hZQKbO;Zm}smSaT z+q>W{QL`j+VdVTgKIyXjiO3t28H;0R)Z6HJ{4Pm`hF&>>tKjO&@*JhKX31gHB7Vz1 zVV|kw?XG%ARsQq5bNM%(sJ#g^X)yXhZ2F!oToIx*Myg+ER8JXWka}Za(Y0Mw4 z8u^~i$jLClvS!54y#M08X5Ns9%WL?~i(8O)hV91N9^{iO8~$EPFG|uKBV9!*hjfn$ z({WX~Ch9H8tAKswuhQT;gP2oF5sT2^pz6lSBjH!TTE};oV-wyT*w0bkks-@dIll&u1<%{O=;1m~0*MW{_88YTPD*V&fdM=Uq?IYbWVIEnCgCb8a=qT;c|ygloiv@#sZS4K21=aQ4WjskQu8?I+ZE53$4zJ(r}CAB)ncod{(y|JTXfF`)YtX}9V=J6I9$Eg^|QGn zue`hbfKK0SScIA6Msh$*WMI5&K54|0*eXvDgwI`xd_a{!Hqc>zwj-O?|7z4CR@eit zeqzoNQZEwKpV5W)PobJvX&T8$`6~+e__=p$^}6+^myS6{ZTB_%RQuIwBvW}+gm;oL zy$)3^mYjO#oZHGX)#dDGER5?5?>Kass^LlpVUaoYDRP=9{Gg3#L8G6gX}k|hNspX_ zU9r%-+qbbOqfQRJv3IkswGQO7x9Vd%qec+dh)61W>A|BrG6p^CA&9UjV2oBuIJ=;= z4o?O%edk3kcaOcYR_P-(K6ae$GFjr|i#$A!qb&;qjOBr_=vlrqa)(Y3Ki{ia^^1BW zsRAx%CI{v>M^~&NQ${9dkF*azZdR>6^V`uZwo%)RTp15qe*bItv7SDEO}LuPU**Jm z=&r9}YLAr(=#W(8W|!A-INMz_Qe>t}Sdm~r_f4a1ouH%&pA%>H!Ns&ubudUg(C#*G zNgkq7t#g2(N8QB_+&hsQfMV1Bc_?gAMX}+TL z0B1YnY<`)fr&vmt6Xu|_VT#Sp@+V)4>^yImPH5afWXl|HboC~zsjs2+Up@Me9g&o1 zK%z1K@q|KKb93PNs%iNia>bku2V7~s&zHB=d9f-FuTMX6SE{=c65F}9I+6DkJxmJcwCaS%MS9l2~oC{k*+4N5ii1c*{3Dis9XC7Bas* z+`&TmNx7ZjvB8R9QODT2&cVXIc4y`IAfHJ>{;MZ<35qg4_)R9JoxPLE=hZQJEb$F2 zVYT*7jnxA(Fp%v`sYcNI@>J(r{xdc6N;*#Q_omduz9-)6bp1p&-%DhaoP>eysrrNu z7IEJBo88~zkMgu@yZht~5(3N#?vcv1mkqbgtLybtKU?+F%u14~&FMe;bBS4@5sUabvg{3)Y%GFEB9ggZimb{7M}COT%EM2Jv{Ckx#>ph|s#T8R z3v=)*j4Ji1eW5(n&T+bpjUuA)#S>kA142BW-WM2N#`4OK{B_on@HATt$gWY};L29H zCOr3HRfakk?3PrzE`;Jl*lmeEj_sUh_FMX1B}mkG#8MpkgCSHry=R-cHi@DvZYxm+ zMaCf6kRk$+k#MN`(~1p$>}gZa>g}8j6(?c-k165O`F8_E}Oh^F)(wvVk9g&pJzh(qCVNI0J^80(TDKyX@L_Ku^%bpkrNdlSf6`$2BbpO!lFe;KQ6LN!Ob=CSspgs+ig?Y`hObd&D`F)gLMSV$>X zlQUT}A%`%Xmt%-e0tpU%wzc*(<9qE1Jh-EN%YNJ0%xpQ>^ldt#foWO|79U9pTJuWw ziFpSij0fyyeuj{@&>!cX)A#xQirrVbQtV@}08i?++_?;n1|w+WyUj~XfFd+>VK~Y& zNO$RXUVUOCeO2ps8K@bt5eEiA_9VZ#>Vs>f6-mXMnzwmZ+w%ys@H|08iji-2^3IzdZ|$F2w;7_epfoa2cPeC?G2lrD zO%r5cDelEyHsJdiO4{>tK%Ab|+e*FO!t%5?EI-uv1f_{RqM?xm7SipbYGP3~b-TTq zk5Z#$dLEC*OeYV>>T&wCdtc``Yn?R4xhnveUS$f?Utnz zt*$QhT$!JcS>A%c%5Eghtx{Wn_a`Qq!ACV`B$mJs$~9YOn`Z2m`1qum$nP~s6$pdE zsQ%G>1lfe_gLrc~#a~Pxq3Zmsl?3VahdZbhl#b`RF#5k>?=qjFta=u66 zDQOgEB3+qS2XS$A=+3giiN!-L}j?pDKFDK3XE4GtU zoIb^sZvtf?`YqK*O^2>eaJZaE5l5wsq3F}U#BGv(zm*lndNm<&m}?t5wvtOiyrS2e z8TR-bY_D#n)7og=M`!l5Fx}D6XQ`+psK0c!TFYeTF}G8x&R(dx6(-P^Zhl6yk=}yW z6*-Q&Vy4Nr)?V>$p8TBGNn$ZcC&RbIwiS6H<2#fq9Z(fMSAI21YCh)ll$3=Yo*UQIKZLg??t=dI%#4Mw)`ASTO5^}@Yr<0# zOnZ7g)fj#k#yX!AW35Qrr+#bUfDP79;oH>NDix;XpcMDU{g)c@4cZpzZfZK=JzTzm zRIj$-#OJ;mq+45z>rSV5Sv>mcGp5I~de{?Wj=qT2!pdJtQI5Akc>B%F@J=_MY{Fj8 z`}6@!g>(@YrW+_08N8-t@f5?@dfJNVaw4^%eWLos<4XHTx1JC#5)MmJ z`8J7v)dC!o@OgO=&_vwfCXe75KCn@8bzheE`#QT*qKP1g*$3!bmv(E5dOlm-oWfR` zQ{!%pz3A0{vh4F)NlDCN#PwC4>2=mf}wh{JX_f1122Y`Ph705sZzg8cBOr>3iqqzXoocDj;Vt z=C7<&yf{~+4KT(+Py<9eS7B*Q(W^~Ss0)7)9lUjla0dD}zQY!t;gE6vw+3W6b>5)9 zk#w6CIiejL_RVWuf#v?`7QN< z1F3xD5$*}~_J+9_&^7j308lT@@tSen`l3$((m-@Jb$);)Q6=1N=pi=& zK_&n64~u62cc58cDo+~YDPL?i7RoZ9ANM;B_TLG~)AboPiH%aNDIZ9AOk{tAR^%aF z9ex@u03fQZ7Vecj>C@i`2*D&(qF7ik_}ANbN-ibC8z48Q;5Ab${bY&Qg&}|-Qvld} z%NLlE+N(Y(Ui@7aCXOmUvf%WC<@6&Zw`nBf7j`J`8qgOsX#IncKNe9k+^v?M4H9~P zxx(KU3=!c>nJzuHUw{AZU#?8b7&FzCvv}}R)v5L2pY9U!zs_u+n*C1`{&N@p(-;1j zM*i<3LQ2V0%>(NCqb5GzKV7MaSj?0v!~hP{};>HdiWR1;=fLs$A|;?RrWu)#O`%;R4BKi;I~po zZMU6U$vP^Z``E~~w|pi39~u*@^fqt%vZ#Q7fSa3JS_amF1Pj5y?l;2-c|V=uXSw~g zN)f(-)?+Km7`M50q7*$%ZG-12?LvkSqE+v&#O!?t2}ah{>ZH?^-G2L zSl!*?%KSpr&BGha?S5GXHO7ppe0Lehd^pVn|+{BZobi5m#40Sh_Evslnp-|%}qo zD)(MM;3!4ea{07;|F8FnML(b;+V!~C5Hf9mxdXFI;5XLgDePUF(MhnW#cS*9>kWn> zh01*{N@(rlD_c1^^obQjXgPH^T70VuQRJ9cfM+Zcwq;-zCwF=mc?XvLY;0|xd$nE^ z(l-fXgs|p1(8NgWO}gZ4u!5!UY#4L?r;4}mW8t4+}RVaJyZEYR2zZ$hI>glx+O?h1_-}r_7k8&zx$^9QgL){TIN>wx^B?Xc` zdce4r&06Oi*Hx%E2kXdVkYNzhSoWnljVLi|_~&gvS_fj(TMd$~MQm*drXn^?@WMX| zy8DOZ3=w!i%+;84FNX)H@6;X(ax`4xC~L;}21QbuOsisWq!0QkfaI_F9sT5}9yfu( zTL&tc+#5*&;~)b)r>blHEJ^DRDIYt>`j$ieSAl4_VUO?CFM}=4A&3$se=gC>n}HH* zMkW;JnQzy|>0Mce+$}KUl%2yRm*PJ7X5n(h*Th_x-YTvj#@o;dnL?@Kt6(;n&laIq z1K{&A?r!WwL{tLKYxX!hAnpx~f@E1sejzV+-2qWr*y^%=B9reQcl==5eB@t+-FCTD4b!|3)03fLN0B zi4zJHsvEFUF0cPy;(jNFooe@p?r!g=v9YoKqNw+MWqL1*%~tcm9~(G$e4{Ww-aFL> z|IH>V9z9)nxerDAN}wzsSJwd|@?-GoehYT1TMQ2oHgf;a!P&Njn0S&#=Hr|DK3X;w zuG>Hlrk9%ad`EOh6ZY~lWwo>62nk}Vg4_g+?tZ~fk*9JkE9Qeub+3Bt)hlHUEj0H= zIycdw8(s*}s0tR43kyGa2y*TfVM8z*y4J%NbHsgK1v87&Ps;!h%Ut#6fF znl@T4nE8QIwT)nWyoH8cEL`mqEH%H%bV+VQb_wYgssIT0`|R1 z_N~9513SwSx8r=DF^r!!`+fj5xE3cMw>77?{)ta7ua0}U#~FVC{ z|ATr90X<2Ee$&AJb9f`8o#>_1D061FDe z3w(q`s&Ig+zeAUM?vb*O3h{N9WFnt(n0X>9^w&sHV!QcLc&eF#l$eZpz|-Dt9cI{P$+~QL zF;7i0K0um|6ENd({Fe0Hw#00RnS2@9SseUR_sp~J6t1?vSRm>zj8;uGXf$l9YnO8= zTIev56KKd%*O6$fFZDJUwoJG?>J{)Nl^DTn|9low&!$1Uo_@%#^DFiIng)lLN@YER zFUbxbHdgePU~l4=A+ft^b$5DtWI6=v#OpqnPswfR*%_CC&3iDry)a#`d0&>qFYrmn zD!LO|WA&i$$$OC&;t)Q~_zzIl6sEO8bDt7RB}+WbyqHXd3gVm!4-S4SjwqvGWi-5E0Y2G8ZH17FivQiU!ieP4As#xn=ew?71WGaMHk!|AjI|NgJxjR`siYsY^kq+m2!w&c7V%Q0&zPHZ~ zz+eL6t1x8fAgeO1S}}R$#&b@1?{4~S0ex-y%nSr&`|G1+ zaSiC;EYrAsTflu)d(6udij85Gz@mQ8i8a8!>VJja{$OSo;jY;R^Grbf8IOK9TjcX% ztC3CCR;O;q{42uBL2>ceT^8*v&&gV+VwV0D!eal7(yVfcuh$6e%pURFz%ffQV>7$O z+i4Q~6_J`}IFL5=tqs4K5s0uW_ZKit*6})kq9P-DTmI1Ik9qzwGqD#I1hm*GR9`h% zU|R4lH?(`xtoCYK2yV^7lx;b@xWv+6#C|Z{9N2BOou2H--XT^6IxeDvD50sYIkeuM zp4>)vC_$kPRWx|a!S2(-N2+5jdMeV;`m0F8@|f-5??Z8dIfusM2E^N=1Nw?}nq^L4 zLAwt+?5*iH<#c?35nk7)VEyujY6Cw0)Zy2NrMU7XAxsh6rMaQv&wb9s*!j_A3Af|P z?jfxDNGi32cX{b1bR~mz>8H}on2gp}7v*sFz`p-$ov-+XO)!F6h1ge?%QqWW368GE zXFlW5o~?t`98~1UCbQF|$1GMRtx$VENuT>{h0f2k;r?2ljkNFL3URUrU~Vo&@WhTc z%qM8>acAYb#}02quE-|XD`d%@K!q70)x-QslGZ1!VMDj7xjB8{lmJ^ViEi_;F=5y? zYR}StW@Xm9sLfNoWx|)d8haT1+XnS7Q-M^#2|i1@sM`yM;t=T-K`tCxr@kB|l&b3; zP}kwfYYclWoSyQX{Cb|)&Z>=}za3^QvCq!TX|i^sVy5+0#dD*RzE7_sT^DrEb7?%U z&4LxX(SNR}doY->985yKAmw`RueIQNB+C{gA8jl$eJH>7*I5eb-d5-UXPoTg zJ-yAFen5B^VqsyQq@CQ7jRLW?uYrrzZl-Ut{&Gm&n06$Q@%OU;e;sm+QQS^)T^JI7 z7W-t$rQc^jX#JwqR$1+(-$5pSo;##muu@%*Wgg#F-g;dq=i)b?7ITT%v0K06cb3E7 z+(lQUow3UhDhtU3UipTi`KS|W9c1CCB04utfy2KJR%cqHkptZKNhgQh!DViL-w?pI z7}fRFEx&dD)B9NSx3XvGGoi{bZZuiv?<0`msYwxHVN(j1W7;a;xedPO?}W{|VqJIZ z5joS}%_HjBUoo)3;m3=7Zi~EsZWWQc|J_Fe9^ygGIo{?k0N*t80F%Yin!Xt1T_WtM@7{iAmie!cQdFH@KYAwA<&GS{k~#2x#*XWMzW6`hhyUc2 zmMU;zx>uEu_}_o_@1I~$r$QCYENY!F_}AC|>3#Syvxoj_iz<^A%&fnE_^+Q(%oas+ z6}=N$)rd6gZ{CMjyU?JyX|?@oH~;Aqu~KdrORl9jmOgR+*xXo5XfB`Ao&57Roc`T=+U(If(TwkM|J+~W~Hs@9-Jb@L*YLb6$ZW>MYs0d-rgwpkY zd;4!AD#a zB9XxT!<3Lk$?yC!1>J8bdEYC@9P8U@Nxwf96hTF<;1ylAiToCuMai3>A6JleVxhfz zP1N`cD&>*$dP{}~sko_syZzI?_c{Dl3>c2@6;P~Xt= zew9onmC=fRSIw7Xn;xxZ_RPM*dHpf1%%$8OpIATDSld=djcs0?(m`gxR3cQi{BRN0 zFC^cP;b`?7e)#0clxpe*99i$fjoME?PysI}u+*XibA*F@s1N1`MNqLVuA$)KlouP@8fg-5z|e)F=p^L!UX)jxZ1atvk2N8nmQeAjiDFIEl{ z*8r6y$mjzcHgf+>Qq~RD@2{gX^WM3X3JyLH_4^q8-7>X)uEaqo@(Fi^X@MZ&iH*nN zYx{cF72#sl)}^MCH4zV1{Sx}qFHKOHhTGZ*r*tUyb*e6ga_7jdC1`#%1g)z$%be(! z4_&(;aPV0MBY*3%4c2ne-2!6L+zK!%jHt92$mndEOG|In1DAFZ+ z4$l|bIo>G@e31L<2hT+~9jZQ80A#s%gWsv2hVEb&gs~UA4hPW0NCK_@7=ko-EWOJr zgs?4B4pB9JIwqpiRyj_OZiAeYCIQKTqxO3uO_d4=xuSA>i!yWD2MI2YGX!&o2P?+z z$ms`gmmh!}B#WfJHC|yQ&tDV%v9~#we{b}Sasf_4jqPZu{o@16CE*|If-)!MSA7p_ zqp3csd>H0lVh$Iz}Kwq*H3MySYwRn(aZl>zT31zRherAf6Db zWSXC5L3iM`A*_jVoPajbbGmZIN~3#cQ^T81O7A^3Pf@%Iv2BAutLhf4E7R9;+*ojb6>Ob+2G9=y zluTnJlHEQ7)>{T@6(o!eM45$T)UsP#&y~6X{@np%uEZ~Ho_S|_rHf+3%yui32SuEh z3Y`*OF+S!a%b@V1*xhU3i<-22HtbWvFM7F*@$_QVB?F#Di(*CK)FFXQK08YRKW3aP zT8GRMRORQu5^fHH>>rM}{mzVoN?r$<80qr>KCLigHj;Qu!{9wP z*Z?*0XX~ck;70#m(pF-n>eLn=YPKl&0)V^*h%3UYKp_k!bB2^SQ3y|x4tcxf#~Oz( zNC7s-TkUGkWK;S!IwY>1A-qMW0r5uz#ORY=Tbhhmd$LCy1h0G#+0 z#hs720cf?0G$uEb8RF@rbOFG0l=c0?U&1F}z@ZO{uGW#vqYrux64m&d4G+aAJVXXI zhq&g<-q!YlsN+5E)kE%={{1mA{tCp0AMK)Xf+c)@G=L>n8hMUQ1uu*HM{8!(36PzX zfE*-+hIbO>oh9cP9jMgz59t-NH0nA5%R00u1QW)Jk@!J+iQ0Q92iyWP&dyenoSaC) zpt@3ug|%sz0Tl^giYeau)<-n(iJ1>~-Ofw%6C}r^8 zY?prm(04>2Jo*=@zBJLdvdLd(cKvvC1}Qg+L@^Xe8B%^@Lg)2}Xm=AM+gqGMJ%PoV zsh5|HA`HO&OZ*YmHw{#HG;X}0b(D3PTOJ@YkZzYf@*>D)D(aHv#y1gt_MH0+wP36f zQNm2{R7Ah%kS5P|llAY*xr_=u2lqPuTomazP*Bl#<=r1~cY!(stvB)ZO_Nj%5v%~! zcE5wFVh=#kDYZl{OrfQ1?l+&$WIQC}J5008nz1{YbXE9$7;sb0)-T}O>}r!~$Ni<< zdTVpL607G)s+a85oShuXQ5;LoWR9?09ElxuyQ`5iik2Y7$Yu)G4qGDoRwOIB&0J!f zU3pRDT^mD3^1?96`UZqW1`HpAtb_?4(E1gC*O-YTOLek7!)V?x`~uX-JJuhH0SEOk zN6tyAX}v9G+=(RM^qr4W;QoyHkZFcVq=m&7fDqDi6-MI48(oz!;Zy~wiD(Jopc20) zBXzec>@&TXV_aLkdEc~fl%}_Ryrz%jDr)rE;(OG#xr?IMfg3{Drm~qAyspi*;K>p* zq3Rvt_-;70ngPhL$fmXKC7DfS@>M&S>8VZyYpB;n@+pg@_3s7umiQ;y+yfONf3C$0 zn|8c9uo*9;eGqLUR`HqNzJOA|W{WHIIK}_+=~mE69+lMDPLuJ1OS53y9lwJEFNKhy zeDI^gc_yQE&756ojAUX$6gGcIihS|h0{=st$}Ml8u;|R#Q{pi$q+z(-WZ*4`Gua9- zfs(zBNLn2fk%-=x2jShasW=HO%F&z&&4IS9dh1?f_!Sn~`Rq=9@AsbHWqUZIZOdvKSa%7NK~Z?S6-RMIOb+AXLgICXb!*zj%8b z!KK>0(Mc?X2MG{+PpYZ=p$wu4zFbv?R#Q#53mR!+OC?KZ6%q&sP3@a4)m zo1tcoPX-l{Q}B{4N8Y8fArNer_N1AnIaJb|VV542r3*C`Z{G=477g14rJ3bAZk*;Y zMKq(5I;k7pz{5{=6@p$A4$YcG&a^y+v|^;Yin2IBb@T(pa)GWLzxZ>*QC9{QzlnDU@du9dB=sw=vnO&m~32$X1YR zi3T{Y(wsvN7B2HjrrRU#Y=~N^dLY>?IOoKj^M&G_#kIZg+xLghIbpqSG{fO!{QudZnN2x z+S(bt5!!#Vn0PL7Aa2@hj?JQi>%~`Od0mM*^UHdO$o&y7_qD7r6uL%vN`^5V@eZbo zEib0zx-WQ#21kF$I*&>oyQ=7;P8Y|U^Z`I=y9`Ax7 zl6heue21ivM4oD3w$>x{FsaQ3zoK`H`PxfC=>W=l&}$SUr=WsVzrkUox)D~h2=fFNklsehXgBR8Ql;Dkp`}u?y*iZa02Erw7 zk_Cbi_6}1UyZ)nZ0eI#6?g})c7Y>x17bx_!J!S-Z2L)!omVHzi&FPPM@RDrnSK@&V>^YP=J&e3R>eKe6kG-g#7d+|0lMT&=S$MiBPg%~Esz!ld#lZ6Ll>PFdL`r7Ekr|QA_*B$ggOQw4Oma8rO>X zf4Ddwq+dIhrI`rPFW-FV&YQ+E{vz^;=$nUO?ijtf^aPU5luzVxpQ}t^Q+^X3!o1Q^ zq0Hc{mg#xv>#Bdp33WZw50MPI>U15n8n=wQ0~=k${h=c9Jb}LC^E()qTAF0-j1NG< zw7bcc+k)x1pmar$^yl^CH1UO30etQ@Row(992EO5kFOrvY%jJ6&M{ZhzOD1QQ)!c` z{ghOs7XgD3<@~WVis|D$9)9^@rfcawRj#O{$kp>Oh4^*!z47&jKQ8sQ$6dv_mQztb zp+%V@qJ-Oy#V9b!$XoE7QVGjaL*)ne=0nL2vTt*m@=`J+A~s!57;@+Y(_T{4_J-*& zp}4A1eZ*uu)KlS^I3KmbsF2Fn;Mi1GAEOKGM4EEVwk$5>*3YowD)4U#o7coK6#$x#yKgt zkIKL3@Roi0wf6QDWXztzNPKJ%m_d-2`sni1Y9}@(8dEC9TtZMw&}6*p!|_ktE~s%` zQNj8ZjG=et8NaaWi;`noJM)_=&uug6od0D%cW!9y9B!^`w)0DZac@XA_bowv(kw+`rkG1_&g`USDu51=EGpsa z3vKf$A?PN!yta=%!;ajT!A^Zvx6h$;b{fd|g=aUqXEyK2Fty+?t%ORnQ=-kK+0=F^ z-R*A}7osYo8)PbzF%wBAxM{hCDsRR7Nrifm+t6`A(3a!p;thJHK9Sxo#`>juoJUuz zRgtAsl~!tn`C8~k(AI0!9@AtEr-fM&Ir+^g_w}jF+j?W zIT$7>^Pwe+maWquxBk*C71S~9w@iSrNN_o$fm1`%!Rv{v2J**&Eu0`FFl_6nDUHm)VK)?#-2nM90Oz5E-s) z!Z7{%1*`(BQd1ZBiY_bPTHuG#{Opt6c_J$gWR8rmH93yMxM+e@hb0I3#b3v&kl{KP z5*7tt^*X|*of1EqZfCmGp?O0BN}xR(X?@FkT^~ReOKcR~gu_sQ%Wr#=D?eK-O?3}E8>LYY|AEvN^2v#No>uSPHa;f}2EZgz8 z1mF9bt0}}-Y)rW-7uc<<??Ex1)cm_~I!rEmeXsjj^!nt(x4GVO*4o|0rCisO;s@DN06;V&eVt z$9Ia{Y?BKO&cxQHw1NeyyPT*R>*+u4Z?hr8rzG%1ufGRA*R#Mv)-VasP@vi0o6=iDu}iMG6v^9@*~i$cHJ?A1>Zz85n7{3L_o+t`l- z(u>3{@x|ZbdHhY;TQNt(;EuD9e%Ncr$IlMp9^f<5pjnX1m=~S#pUIvhk%vnPM0$F#6T- zzS^kHo1~)91HFm%bWa>BHHz_4yuf92fvk*9TB(0qH&G;bpt6U9 z;4nvJvp7>~qID2q>4CiRLPe@kbN$6(Qk59;49RGRxb4QvBURp8pLU7Ahry+A0?M0Q z!1Q3*%rgP+(sd>JVZlb)!T;qi{L!v65AE!bL*)}!BZck1K8c61v+_RTtogh3z|#v* z%(h0mHInc2yF=KUAxHl&Nmkn}Rwg}+f=Zu4X9@9#&pkbko!Q>*OAW>PA^54G?uXYhbb0_M*3?KmWpKjV8 z-zz1D38Wy40Y%E+zYnj*azJyd+?p~={`m=n696fw(IAM-e1G2@8a;#*{N^X7_fMaY zLcPQYq+qV6z5YKpw;CY@$Bx{q|LKANXTA)-n9v3C0S)g@7qUiyYq|1nSt@d84OTSIOaT0j2h!ABDy$~;v4{lT^|HYqpF z{S06do+5z&BSlVQJLFMxl@I@NCzu7*+cFAuQSG1VDcb0~s zi1V=d4-U`9%Bt(CdD!o-%Pz%W`8MOu^384cl`A*W`s>Wuv2t2AI?IN?XHzRGG~LIe z2LN48LimYZXCtc%@%>*v?!pK%Nd71V#j|WJO^-V8v@&DfLTIivam%Tsh|+BZdUsnGeF3FfxSL;eDIy`ww4ON z2W|iXCd)S^^&amqLyDr)b@ei|e4tUicV)46SrJ*MZzW3Ex`@&nJ!`mHk*o48Z}aU8 z)D8m#MK%C(rVirjzjs)2ndWfM^`oFvjG<1Y;Ozt9;1(nueQv6k}|Uf6uR9fhN<6Z0r>SH6iu3z;s}(NP-& z7E52R>4HXAc~zNK`Qgpp>xUn;Pq*&hXojjYtXBc#hE^R-SU#bmXeP)HTD|y{U$5sC z?1B1#j;aA<5}a*Tp&Hx<7>?Q7#SFi~dY6Ho*$-$iVkp!R!Ze#MqU>A&orU8Pf=;wfI>e#^Q7DHZrAJBp7p~4 zF>4R64~pCy+>GF+FYcbLs7$BEK%?pL?y2e0gvIzACR!lT1%5wnMcbAc*xj6ZE$P}k zpRjmrpJtvc0^tge)-mZdy68||6*C#Ci6P@i{R*nNNG2ISPCrm-v%nRUZlO_w=PpDL z%>g&n0LCU-`cCZ-qY**S5nH+xbc7_$?`wmgz{12Q2F%4^P&#!(sdp`u%H|vD^;ao# zHS&+LHg!4%Zm@5+$=C*oRPla+!%dn+I`Cj>8P77r6bGW%zDU0Qy){&vhVC^5kNzr@ zaKrs9CaZ{~5Gu7Ux$D@B-ZhkG@abid2^Tvxp}>!(6JHkOG=)I&I3v$CATbh2Bzo=s zy+;qrll1K+K>0m6MG)5vk@?si+amWLAbBwQyaz*st4YB>DA>&W{>`Q*V=$a_c>?Zz zu5XOpXRn)&@9vf#ba;k|_eExGAw2jK8LuIV=ZjM(RmB2A z&4P0fCqQ1csbK-$AeLxho$Pn*8JmOnDFT~*6K7Y^P%4#!6NJSheHpYT33fM&{1Q-v6LNo&EAc-g~3?G&QSd(bVI;^=thUdR6o{B3BvMpSdxA zZkHA#-RLP2@A8fkSTlGAfpTHr!`wISJ$Cd8=iV&x0fg|!(s1Atr%L>DxX!gJz>|N5 z$1&m5OaA>Vt~eeRt8^2Tej#4c8o!95I$Ef>jm}&sZ3tY1DZ`vBDe}=H@_6Yg zb1Vfu1HX^f1;!02U$S)|MH6?# zqV~GDP%Ks&+^nX{mO`8kTX$zwVv@q@7IK1chi{*!bd)N8rlV^)EABN-xrjdxPi%mU z0y71a22?-IRjDr@J;J9ie5>-rdxaI}+J~L!`vVKM@jf@Z7?IqiyL?nFeJWV-Rw2(uzv=!95k zdgRc@F3e3wWVQ(P<4STa0~`|GgKnPqULkVB%z`btTRVx6N=v2FnlMV{@uR`ns!#V` zGsaaOU^dR7oVj1O@}h_uLSX>(5!90Mx}$B3E=Y}!5fTGK=JG%j)lNk6vtH3?JIXKO zcgx>ndYoD3p!q<2;{tSb-VY;8Q8bgan54|8RFJA9=x6^j>HDDYk_;_s)X9BV2;`!z z(JV#3S~134)Q@-<|MP7BeW(FuWeykzmbLkbe(>b^_iAkGaOJJ6W|=pZP`_J{1~oOp zO2JPm$-te1&uOgv+^=GgoZAfza3_nBWTTV+;ju_sdNO zU9z=#soa^cVNm2It|pu#%!CG9PmhmV73DYHwY zyF6WUG}Cj-U4QZST~Sdb6gaUpMfOQZVHU7J!s@)UnQ>E;{B1;p4UziBIys%B&$RDX z;P84)I(6pUW|!7PIh(q~aNKsjUoAH_{KDgdlA5V&bKUm?#%3cec*u-u1nun=5o%BQJlUabCu5@vI+HWT{Aj9l_09cl$to;vG*B@W+75&AaEsO@w=nfR}; ziwMJVq9ZMA%bbzU7R#b8FY@xArVR`B7IYlGaD$CMYt-T}!QC^udFbnK_%)pIsR)I@ z@47ErbqGYwc%K^ls}{ht<^*U?5yd0T)6E#*DYx}rhP`pTS4r7*!@JUO@|E^uySS#uDoXD4bP{MXy#Fkc^N7FnD47A6<5c3<7=I8=_U>1cCSR{ zBKfo}Iv!r!oiLF3kUGkKv~`HrlID_5myGp9AesAXI~vX};(*>bH!0fZ((*bT7AT}X zj}`E4}a_w6a;}<);gl0Ii^YfI6#v0k<-G ztcsPiJTgP6;scw=HD8ok=2Fo1j1#<8RVX$H-Q3{18J)k~+_HFXm?$_(l9}Pt`A3xo zs}Yt^Sre;+3b|6wDocGmCe!}+!xxEeaB|)1%wV9E^bX?Qq_sJ3$Z>Tz$3oiJBq_zl zQ`ZA(14_-)@GNZ^R2qn)s8xU z^RNbkpoNqHCtXezONX(Y$h@abp^A?cgAz+lq2V&M+fzxkAL?GPDA7N8O=vAypO>;kGl@mFcR*yFwi(v}iZP%NvMK68u$6>4$Y{if;16Nr@U9 zV7tfzEF47xS7GPNq>X9b)9lgBn36pd#Jx@Ff25?_svU6YgJ)kZr{vNGbIxeqeT;kR zc|?TxDXzYeZ`d?yW)U49+@?xAeg&phF3I%&H+e4*+4Vcy&m)gN7JX&cPMtO0V} zd4uh0v{F2p55gnqUf13aB%wHv*K6f=UU-~4*WSggnD?Z8Kpmxy)+SZ>`3AG!j3=#0 z`iU9_U9AoSQ{ee5R2$+1H-<WB7;vN|`WF zwN3m#?7ekVRbSr+C`gxdgXE>*(y7wjASK-rN(&0o9Z~|)4Fb}jAl(guG)k8uQYr|- z>~rz=zR$cdGylw5GiyEnsrPtdpS}0l`}?Vz6rGzla`>WRdiZa&pwWl(Ilk=?UmvzL zPurhc6eXnooGv*+nYSd-yy9wpEciz2UK|mnzQZCXfm0uaVf^${@f>$aG689_0kQXX2EB#I=g;OCdy)A^YsIKSI5BWPxpu6hxR>x1 zSZ?Mf0q6+|%K<-c8=SeY%8Mf%lDtrE85t17o6Z z+qBtFS=_;}^Ko)0w2w042=sUE;wuR^uEC1R8?b!r6lEPPYg<#i(E992OQkFl%akL& z6_q1Htf)Vl#kv{ifvqo)M*Q%OQ2)S8|M1NDlgMG>({=iB|v3Q#>3Abw^cf%F! z_8e&I$jM8*6IUvD2uH4z5p{mRBz56s6n32#ym-VYJb0(Yl^oUoOZWT++nUQd^DbYM zQq1FDz~qk!9u<0jt<0@BvvQryH~vRvT@8wv^O0`+xq);u&ZJUZ%qVO}q#=livB^V@%DspB9Kl0Sj z#<%F11W)_B;of}o-mh?&PP#1lHQ4etCkQ+Zg6-a<-v6fgdB z5CK@*oo=I)o+EBAjtiqK7ap3k6!e3Q)4n-uh*Qjxte|=$4~zNm*S&VZ&--yZWP2RM z@?6nJA)|_Od@8-H1jWYo61rm__(Ec66RKTEwHS%hLH9uYXs=E>AWO zMYE8J+TJ>IX%dZWW>{Rj47ozkFcO46sGG)AG%ebP8` zEv*8C%4?z22c2(+C%IMLtrBWGhe(zLz4W2OD7!s&bb{~O$?0e19(I3!?y+0QY0&)H zaCn2gN7kn{>!W;I@|#mEv{-U>Tw=y@)k0`qzIm2o=84C+{Svtc7oBA{{Es9%bWm@n zcd6UT=souXZ3xvi#ZX+78LTS=34Mv5F*W(Spo9}LxCi$Uln$Gk#LOA+Z|YVgaTkOL z74SIK^=b@VE?pRw?083DkF|#oqC{(&Wwvz2*qx;WZ#X!1mb{7j=?%NDD=00Zr|_{<^`NaS5J!4l?xP zH&=`B6ydmQjbpA8ED8f<{G!68eZx0h@6`FRbFzmVO0I*Cc7J)?@9=U;KlOpmq6)H$ zO9NK$By^!S93n;|YUJs3l^ZtLmMZAO$h0^P3Ck=Fj<6VlqLUue6iToW4bD_+%uhy1 zMLCM3tn0vp6(c_4Ef5%C^jnXZi`g?TMEMptih5dZ;tz(Tpk6(jb62&gk5RymS=vx^ zlOHgpzT5MZh)(XshrU~WHQ7Vl42_?|%3;%Yi_980F_*2Nt;Cx&$>YM=8~8|d8wqsW zB3)e*46C||>x%59h4-3ebIC(|=VU@E61$LZW}pz$Uoo!gm(mo&R?REsk1!+IYK)@& z2D&=s>5aPadY*2-3b=2q7ulT+W8ya*I5|s?)1u5+6K|q}pM7!mw+HP^lMhM*Hz__Jk)Z|LLyir<;Ul!pyr%=x zN7Eorc9FCs>gcNzMHkKVp)+5HA6pKgcJC;+@ce~}jxZ0tjt((QDrpa#b{fd7obulK zW~G|=HsKSyH5ROr{S^}9rY`k^s2FYZakliJaxxRTcG=4qZI4VF6%Pt#8>|SeBcl|f zgy&xCiFUyT>`U`$%T8!RkM9`-@MZ9E=yvNkGr4K`3 zlk?UJr(#^ZupWVG{{UOh`AGibQm|D1!iPObZ_R1?@-j2tNdiX@Q_WE0O4|p)2B(u0 zV$u!PjdG%w61Lv_?>-gZW&q*<)PAi5N1!=rDLMc6_@*za>9 z2vjqTaaM;USjQ|@FJPKZr_Gt(5adJ%ghw!I^-J!hWUlS>~#<_tJ05R|^FiO0) zrVv2o#d8xTPR^r(V|PYICi+5lk1mc)N|C9H_i*8)mr|sd6A!hkBU(-LHga&kJnq-e zXLelI^|msCjyPsb)_$sJ_#yWA;up+nOQsOznwFt5cGHK%DKtH<-fSh7S;Q2$o(3O# z#ABB0a=8n4L>}{)e@M=fB>%?B7`HXYW~oMs&G61gkjPZui=v;akIl(Y)rOw*mq$gg zW&Y)bq3Ze$O?0|3=|-|O2cg!(>MoBLe3ZAO86l&PCh-Y1`OvD{FK!|k^U~<4+oa*Y z90}tBqD&3mW0EABrc|nMf;gRkhJ%d*@|#pG?;`0ZsJgS^KjV{w9jqt%ZpZo_V@_F5 zi~bR%4>b@est}GEz=9SF_^_pl0S+q7>idR%Og$p6{0rz9gpQ8ZUmOm!8625!v_BHw zW}h*v;hi7uV##LEVC?S=!QrPUfj=@n!Dit*qop7xPbu=*EEM0P;yH81D4#lltM8u&7 zp|;)T@g{qQ9&p_)tw0UYpKRYsZBW|-5iqJfDhg!~eq+$95VqUUJT4}#HJeR~~yQKm?~ z58@$y1g%7$TcJ=n^wg%!vJoM1s6`>JWio51s9hF%fIu+pAQ+_S*{}USzkjT6-t+8h zTIiMZBr?@_+7vzd*;P%S$plQwmr8eOoBVQc4_Z1-t5Y@-qP3SAH>$DE@+j z-$X)rRBIl|A7(u4vaW@UK#-_x^dAz-H!mf%l{mx{rW|$ujrLx*?!UaBf1|zstx2xO z;lJqt|I$nT<&Xcr>oB<{qEp|dN;E`MpZ#TE&<&#`f8bp9+rFk-Y!YcI4cpmcU(Zp2 zSOoUuLQV#u>5YchU&reY#@~tQpOz?fM|NJk6u#XD9C9Z6=ifXKW>-yv)3;LEsa1Vo zW#ITPheLSiKa-;5Ym_17DlG>VuDs6^c`~5N`|WVO|7}R8o=Xg;BS66m+_%Th{6HWP zBxU_q7`z@2xy<6-i!zI^r+3A%5qGCKf8Xnh47yq=31F*SCHntWx*B~7YyJXo-Zp?D zJRge(($xr^=h;46yYqE+Cb2V}h#QpTC)C7cRGqG-XxA5FMoij3ms(rC*YJCPz6!c! zaLVWCm)FwP-=P6FSlvV}01gBIGhT}^YRF*>q*w?x!Z3D8ZcNxI-qaDD5>inYuB3U4FQxpj9 zPQEYu@6FXLMgN0TnK-KKy3^GLwS{l z60LamwjjLd-a62BeFYRxfSYdj=X)#)Wq`8L)+}6_{aPq!t z0H=Z@Gk1fV01gcqqgiKZS1La|25L4F;E8u&3=&~HflL<)9kQ}{X=0;sw&kr1ErK>N zk~ckU4NjRM>0*xmWs{qXYNG^DyBPzOK*+LC(|bTusQ?K0)%ke1JTSqE271lxU}EMz zypkyrePe=QY7)E7i<*8~>>ohzb|8Z%%Cx!Qko_9~OD_13@OtwJn`j%|A;$-Q7uxCbbr1G)D2dJ2jCo#(FN=)KeMbL@dE-$oL5KEFgcZ-JR7t-kVh6`2%Wb$;(#nq{{NH z?n9^kC>jV1V0-|>;tG_l0d1kqmio&(*Y)(71z;2tK&bYmLG3;J&mhl6fWa>TX)<{W zh{Nsz0M;QO0=Nj7;DVT{K@ib|e^0RLTjxqIN!;Nu^FmY7)ANdJh4LB7kf&IIY6uPy z{mW$mWJ&hcwG_?ufz}X03@r1{fR)Gr&|b>Y@tWCzbf&*>IQE;( zS*D_!sTUw4El;}hr)UG9FAymC0sMRNyEHbJ$CuY744JEh)<1xa-2>p|VsN>+8&U~g zn~%B{{hq|o)e}(P?`L!YDOpF<$DnrhWB=1LAa9c$eoet7ag~^!9DmBtvR^gwnUSOA zxb^9eu5mnOH{s)l_V#oi_}kESYT``~iXTAb9ZBROBE^IcmTQXZ14}?x3E6Pfx!M7Y zUO{-U>4o0_49mFGWlpAbkd%%lT;IV$+rA>3^sRw=>-*gEDkHS}={2b+@(#2J|qI>|A;U#e~ z&8@(Lv;rjX7?$DW1^~y+7!=P3K&J~={D7+3v@su4G6tmQoVn55%@}uZt2B5@3l&EI zQNjz@4E3RXkj1%gS;;09cmDI+uoN$wf(&E;7$RuU19h?^_P7l5ZOfus7IHuY0K+c2 zvkv=Jv34?zaRoEurbquNBwD9>^bX0&P+WNX!A1^Zx|>nUiH!$s}z(pAqHLv$YgRW0A@5T#F&fHULw(2??JFlsrjfi zS?|IvPcWgtHd4|!yQ(JC8JdoeLWR!=yN6$|+#S10y`TaOCA|NzWg8IC1*K-h_unzT zKsET@NnJ!R;#o^|Yz(nALP=d-TlMh(rE*MQWOx@~MjRP;72KHWAKRu6cGFAL5ZxrM z+_Z@Jc-tE(`}yyOLxUwFeRh|H)TvJ{I{+9&OKGhka63D;RI-aNtdrvs%#@UrIJar5 zr8$La1_Mk$a8py%`4MoS-+oJ>g3R%VdN%_|_8#PCP_W$2zK3y2$!9$P*<#K679#YQ z@f}PAi0(tapdg@Cq;T7hIOj-9fu;c{lj~fDJPR|Zgv~Sak%2s;JC|p44y9b&3ahn3@8h!UiaXgrmWWCJ?&#(5mvBFM#595hjJe$`(gphwTCQO^aAQSps3aV`` zGh=Gcm#_6UqEIiTmh#13qEHiZ?RDO?%e`h+$ zV9ApTMq7ogV505z3bz8|#dy_MENF`IoBL@`^{w?)Gn{8L zb7w2-uq(l5qpUr%FVJ6kc|T`Rrx2F1FI#E9v5JIoC8&zYPYJU@ z4H`lsIs-5kY9!8OzC>V#5E0>oMZwM-aTmG23m&T5hXRF!jf;Pmu&i;VaF24(1%2_r zaJ{8?`Cupzg4xU0WG56p;{QVWbf_Dk^{KQA#-b=*X7T&&Y#lV~IH(7lvTQ_JRP!nO z;`~&1Qw+JYD#tt)DSW;fs}^NmqeHFe(t_&yWGs9pOrBgmF>j*!Hd)1UHK%h6&%C&A@J{<2NSeCV*5SpT zYBza^rw$F=c_n!xIyBBbfJ~NI;pL<4{H@NhUPTD$o&~)wx0dX@*bt!&nM6tN+#SIO zS$Njckp!PMTx#eIpY=FWV7V@tKL7j=7od(%{Rd!7k!2FjGrd+#CKZo4&`HXEXWlTm zsK8@o6O?gOr(RbZ`)6bfM(X)Gl9*&EJR0A!fK-B{k>>CcJ;paqv;%XPlzrx{(#@b( zYV?&T%4oM}M|fMS>C^W`Y(q(}_|WaSsNN|Wblw;!qi|zg4}kpyQdg4dRiE5W>>O$t z#i|kIyLn6a>q?Hm_2HIDkAv!~`;MeHUD9t9)Ale`o6O?2*CKZx(-T6v8; zy=@rstbc`q>RS9sjqg23)vAWzQ_=;>V-hE8RVouG=*d{srKO(ccIOPBrxTK*Vp?pa zQm1;%wGOwRS|8%|V}E$mIE(+fC!1EHH9MB^aWNSm>b7{h(Urc+(*9`YL#2dk z!o@6d^(e9nn`UpdcEx8c8g@iQ_W$t5)t!jgvQgd3WA`sln1Pi z7$LhFoqXOY2pTfo%dkZljwlnI&N3R?3d^b!k=BAxr>vPIfYVmG9>Mp3dwGQwjk z59P(pZ!S?EZ8Lw!1)ZHBs7n(l$fz~~Ce-t;gyPZxZkIS^{q66b93PwYVh67(e`h5- zyRD=_Aj{;BH9+JR@RhV^?B4KwgEv|w`F%BJ;cB934awChN{XKLIde6cL#6c56*c$@ zqOAvZ1#6*zFQV&YV%dM|)$4h-818UK8OL{ZK3s%{v-YISMXXL~=I@LJN7_IAqF<;g(8Md&ZXa zN^cbzH*}f4Q1}aXTg{-dsH+Ot!fFS;Jib(X$Fw|F6#ps_hK-#}5Jp!v@jTLt+lO*D z`rr#(>Yz4*l#0J6Fw~-OvFRhdeC!GfX=8T_rJrs@9rlm=KQ9)Y5mYF!i>EE&5AOaH z)0hBbz)MzpAF_6YkAKl_kWG!k!;&xUQxWN(62ap1L6gw59Y4<90mfEXpM#8TVfB?) zOKUENa~$SKN^Hfa1qgfAsfGBFFApONoHz3GnM$2RFs=x-da>Z{{ef{&b(pM%EGLA@ zr(|6HMO0X>0^z=?A&Gf^4m~yUmNQo^;_JvFby_6wXZaIsRxLQ{M~KjFJvcpFzb_lV znt}IVd`;GEL3Of2s-r;0`x{?f$L-GW-x8z;wlUUfFAy7+TwmlfzI28-W-kQ+fvV5U z6PSVO?I?BpP7QfRybmSu=MB0m7w3re7(Sa?zLr*MfwL0iH-dnmJOv1A?%_^s4FRus z_*D%m6K6%E5WDHSE~Ur4;VX}XE^2pF_ID@lzTx!?%8O{+dPOf_8r20ed>yvCOpQ=8 zmC_>_=P?AR&zI8ujHwH^X*%ya5@GBiowCKZQhO%$= z+=CiNHx7bj&3$`X6I%lVibeE{GdGG?HgD|RVlW`5*dp=m;nhN_Vm_nBRPLheIgja1 zz9-XcHz^LgGb<{XM5n!=u`2hRqw!#+a9v;$gJbZUrK^|QRd{*)zRS$!=HPaI~0zl*Dq;xJD7!P>yT_>O4pf#P`xwC1Yjz#mocG6(kk^ps5$(^Njv|WCGfleBgRo%Yo%9UBB_1|z~;%`=%234aGpxbxA?X!w@6%XurlV~LP~TB zdw%v-D>PP6MqKG*nMI_T#I^1K4@s42gf&L9>)Cp^{uZGbwSTv3q9t{WUy|@zH3#c8 zlK%`9RR1 ziu2ZiWFqzb=B((vQ6s)?+j6}Y_CH$mcE3e`DcX!*O&UQ$*jx%sfd_^BR+d%-O7sQR zmZ7!hVK41Idj{wyUr-Imkc)kv>8@sKW$dhzHMi(oOOvmj#}6S&QDjD`G9TR-(OJ+y z3L&&ASW}4e^!6kSG`gTj7Q8Z|Rq&4J3|C1eWn?-{efzt%UF75TvNt}Zg;-%D9Xjb^ zxRPkl=13C$pRhX7mmd`M95GMi4o|MDjYF;f$*s(Pc z@}Tg7J!8tRZd>LuUfoKD3(=9#JZ))Xp<6O7>{y@V_Fj2eH9z|yO} z7LpDz#WNPLtr=Jum<)W zJ|+1354Bkd-+wGpA+yG49#huf$qE)hJnp;#OMDz8$KlA>P&gf0f`i$uXvt#7>!OTZV zMWlDSS&-Owb79px7*~i+fw_-u<_;-0$9txxD2f_4cCvc+*WK2Uuh;r41`|orHvt?x z`OTQmp6E$v>~k= z&M?M}jaOxZiqa)0J*cU&xc%m{*VRfQWIOS7_8Q2ZWkaGO0Oi+fivgMU)AwM6V*boX0gQ-prl z+$bqI3RjOFS0K+Cfup*;ZkpeA48FeRIEq727>dE+K8kdX!3|oVds2eWClpZ59P9Vi zTu&y^bebH{MG4Gqp+f*BLfCc{O~zXpS@+U-a8HGS1g*6WHSL#Bd}jfSF><*MAfg|t z-d`&vL6W7>Yx<0!`Fs@^{@oqA&vrt8KI*-2SbX-|`5)*nR~ETJ+w0pB;p2FOWVqfE zNFc9Tn;HnMnpr}bb55Ddn_fORMyg-CdaRC617#7F=22Gu9#;^oIDQ<_eJxu_k;@Gm zs?1ufZDWr^usZ69CU!4)vD zHG6)Q57nNOt5SB3t@3|s5(J19_;0N;q8-_(&!vuPP za9NFU^60OUnGt825W^@9TfJKr_GbNjj}tmu zJsq-gHQ`cFTP5CosKU<%kU+j>u~k6cV!mX5}E+xcc?h19%Mh&tC$kLLwIR3hEuwk9@3d^5>Aq z7S!cKMBU-u2P+^+q&|07L>Kx15wd0lIFN(gRgu4Q#Wi&ZV9!A>SB~1{5F%`#a^CT{ zY+uZq5$kzO9mq2!tSPF>zt;y} z6Y$(XLzG#FF5kX(a1MkaE&%iQX@M4ok~D<&7V6u#?w*=Y-`D>GuoYV%jom7W{C@=^ zeUlx4_K@UJ4WTjWfEV~IBt}C<%5^YU zBO7#p`jUfm?x3=fOt3&u^U)AT4WwP#HpoZe=oCEjw=RK>0ONqOgx{)*#|OCOT!3&4 z5=t}mI&kOc3#zBiN$;&X0P;5upoeLYGD4zfU;%tJy;noY?78GtfV;Iu38VuCA)--l zpMsUCH-OQk{{So8i+CA4rKZO85+E@}r|tnL>a(+~F!5-+R}r>Gj3YoAKlW{*A>(23 zD>2V6J%&T{N@KCGWsJXTp95e|OEGG37mSV8U!OA?<_xkc>70V*f%WWx=6Z>3$uP*P z;dPpWbNhf4Bj7GI545{JUb{u9&w<_J(0AS?y6K6lGU)lfypoLOSG+j_6uq~ph&tKj zf&>)>6~CTc4FUS&rvS9d1_@&y)ZF&#@P_^qjC_7|x_;{;5Kog?PkVve_XmREM42^0ticB) z*g7A9IQpI}+1%by50Qp1fP+t@8_NN#>jJnPQX)gTIkqhqVBi7}QgGNwQ_$&T(ec{a zR+3Im3HXl&^Dd8Y7B`&<4ht(%kbEMz@POQmridtt6|2!+qdmNHVdjnly(!pB=vB1D zvt)Tp0$F>|=Bp{;)$vn1-mo0t2@FK@UjSy>|5$}a&jZqswv2=*9Ci}IJa0}b`gZao zjjch_`@C>kb_4!-xtgCcSs>aB^DQ5*VTjo$Ao>P<(q-(Rdi=Kc3KCzSW~@%9U*;9h zc^puz?}NFA;yWarRg3=-C%zBRSYgXnbob=W^#7pO zKlkR3!E5N<96hNysQJuhH~l+VM}2kSrJm6)fTH`iF9EMaNB!<*tk6QlJ+}VMtb5hq zDZTLg8OfLy1dz(u_H4L>QNGuiqIs#qnYcb0CRu+{q|}Hhmb4NxFq5 zfn)~c<3Px1Cz28T5m7TzG}p!~9I)qg#WA5vFP#MMIZERUWSB32dCBmd4dWQFEO-hA z39wrJmA3#~2>QpNe8I6sTEdgZKyQ6W{H0HO6SQm&wR@C6uAbr~K=(T=`Jb7Q8d?(+ zrtDAXROW90L|-;BBDjy9cA-h(xi5!e1ep?08zbTlQB4fpXwy`?iA7G;wtAs*w z)g&paz}LD77!Q93bFe}7*);66-M3AV3ilCMAfRb7%epxy&|&SB>Z*P zXx+JpVc;oSg{87R5%%px@>_g5BKz;w5iYXum#$b=Gn1&|sr|833ZlWC;sNNHF=qQ73+IrwNlXm!Y|=nnur9-aspN+GsVlbFOwA3R*GpW)}%0Rjp^Na)ji8X$-#_g@Q^ z_w9z!K0ipnH14hlUSsQ{7!`up0;z|-;5+m#BUqIwTwjxzwddv~XwpO^eh9d;UNpBz zl%C=CLY>?!V@VxfCHMlSgRHgAfF_&N34sV{7+WEqV+T5jBLtr^ z1x_JuD|M~dq1Y(0yN;Q&Csd}Q2Y_(mO2Ut|E_+)`#*6I(jRvX{rh~3U5>X+|H2lxv zS445;hh%PA;!KHDR3Cxk%g1+FP+5H{q_A*tC!e0a*dmA4MXKzOu5xM<%s;BEyfCxc zXHJR?X$MAO(n}Q~L(joPgBtD)q?VemSh;HLR!U%ZTss|#-fGo)=0Gr*COa?TuG^?i;G9G{( zAqx9~1%NZ}ZY2w;F0AwgKs=DavkB{ux}$E#Zr*LhvL31tM_;H9;}-P~t3~?WjDK29a^k1uCkz|nqVvZ6F7#9AR~%tqUpqo)iH>4=B>y;x z1hKZv)dGM?a=CU-!t}%(=WywNLTBhWnGAw;gX{6G`bC+bwM!8` zgkdR9CyPNSmTGovrb%;Jo2dR~s^%&fr0)UcOo{sgq+85K5eq$`WV;il1L0U*QAw#* zGJW?jFPU*s{SzI7O6^pi66)k1LSWXY-oTRt|0qIkdO|K^^5(uoman|;U&wqOabc(I znA^w;xxww=rn=`c@-a}HPM2;adcZR1wM>ckvqS6+oLl0Vr#wH^)-QS^RqH)VJK9%e z2Eo8n?eSpkM~`jA-JXDHh@MF6C0+Q0{`Q{4jJ8S$@TIh{pm-umi@vZ*tRyX(aaEeU zoc?zW&l(uSm(CrxClWczAPEGQ)9YqS@6XPvPVcjzq@6Cu|BTAAq{ z!IZ{;NphY%e^>_s2XnmFB6r;03Dt**p99Yo#!Jf4{6gYTTq2y*^02eFiwL5hr($ky zBHtKsFt2E|$Eez?$I9oZxY|8rgH}F*^%N(Rn6@vR;s-9;1i&KcxSa$6i`f&Pr1$TR z=(-B8cQdQWW(qw%U?e7+b%=k!seu=iO7x3)vj%o)$dFwvCxa<&vHm(UU8$JuTWTDw z|07?@h{i`pZV>PU%pc8Qp^BXSEO8Vg!?#%@Z=tWcT{7oK3;-qz zznJLzOrfL_7yK1VjPE+Oq~4QfnmJ5VEv021T{Hi11~-Q7qeN0B={zICwx-|9A~8-3 zsvMlxw*kZ({=2t7} zm02W)_5WRvwj~ym$29OMDt${!Ht$J@VBJ$Y;f~P1Yk{ksW*@)h&wK6DdxetD(u#J> zwEuDs7bU|K(n*l!O1wKJ|_>|`R+QEVLh*j{x$ zg&OIRMQo*+&dtnNhPO*7tI4@m7sqF)&Nwq=|K zF^A+|&=p)R8zAC}2HL4hx3oZKJ8+&7KaZC2yNpFQ9_F2*Z$OP?^9i8a#7N=a_m&t~ zbyBw(d1%=IP#T3u2&JK*`e5DTkOh{cb0g#BN;0kVZxEi{@%H zU%o{+zE3?P9w{rYX)iR7-H&2e@RakAA&cWpJt5DY%|UWWG}&X3m4;>)s~mji+J=Z! zX8EjMxw!DGJHx!p7!Cuj&0WRaTDVhZ*FieYnDrRJ0;pQNm@{;!>4cd_qld{*J>)kN z1b_UdHhifdCxm1C_Wr(r2N{3-LA;dlgXK2AK{~^vgb;Pi2*;1iTHUMj4TusFz-Mu% z;{~n47S@z4Sx-IC`{!~%>XRyo%WM!_@AJ3_;Sg${hY}e)6|{50Hv~fKAp2UR69=x@K*943qksIpk+`$5wyNGfS7X*Y1RzbAT##_fy z=!bXfc1s$c>MnkEJQu8ff*6*icLR--s?i?)_}*{_@{=3DfH?4|F}@@JafIP02nG<;?Y@6?RvPAc`o={Ox-rN}O)%LUcanX{>^<~%5J>~!DZ5PE zufGX>{(W}To1FfGpXqOm1jvS?lpiaz@N9GYn=%&|Kv;ot?k|OW^`iAWE-BxDN@#?E zAtH)lD?*f-1eL^Hwlf6t=2qr0ZyPX$-mXwkbsP&iOuHFHp~V}N&&tluioE>C@cZkx z<0E5xdt)bIyU#A?mYtq>dd^PQovYq4F?kb!d>;ma68$w;U)0@}@P@z8EUhJ4n%Fup zL5~W1WaEQM3?kjX;KM1yj_n01;>PRVKv9K82?6wT_)AfDD{0G+u~;>uBu4>XTA9s` z0y_SGj57Q}gz@Vs9>8F{r<$L9R}a)Kj<$c$!Z7>4CNHvTHcPIT%tEQ`yYFiX!DXNv zNtD3PtFn(6 z6lYPNK33t32#yAj4CBKok@sYXXLKM@l%LdH+&Q6qePr~%1wF=dAtjB?i!C?m{ZG#b z{!Q_IN1=OJi(ch_<~9DG{$#BJes3Im`{w@t*h)yK+9dGZO!lgm9RD>m^Y3Zq3Bm7g zj-4I<$ADV!u5?jV#Y%=8H?JT2Z)W6w-h*T#iYo9U^<6ajU)~hmeej6?Zi0U|!T;xb zfxW)EyQ8Dy@cfeV^VkrTXO71Ch#@H{g~aHT%SLDc>dW`K zarntCv7d(-=YijJ-v`u88W?`daa}A4K!!&Bh1vEwpx6V4Y_rC@$f!o=ASw(%A;NZ> z?f0^N4`lFjy`4Bkx`XAkWqCcczYxN^&ozILH(>bd@R#5?-uI71Qvx~9#kxNnUD6&&EhJi2!e(aIFW&_(!9Yu*lmuS74n zh`UCm(stoM@@wb5KX-q`Ryj0#IPgCxVdM97;y?Cq?z_6rueQkc-EVO#Bd4UY)_f6! z{yzNbMAekT=&ujGfB_PC*QI_)7T+eQbA#&i_@E%LBKy4y>{n0wZ~Mcq`bxD5pn3=( z*n0yY0axRApy=cY;+fe=5YK#4;9N(nNf(XG^;IfJ-_&OlNHQqLWRCoH?QB6|y$wVV zDf8s$B--)(jzV6M_;L6VM9e?jVd1}Xu62H9`)I1+_`8Xn!_@2b$I;_@{MM%)G74jE z3*G#%XgONcxnmY*fw_pGiQiEs9K{DAo?jZ9s_P2$t7F zqH!0_fN_2Z3a_mGKIG+8*>#veNywS%U95FNlD zdk+-*>n?tA#927hNJ|KLt#kx}{RU;A8z@#4VlMk0t4SDH71h-CKKHl-2X84i4|+`S z@1xK5YS7m`?gD$!u>Cg0GoZVI7{EN*bg}n)tt5gIy*_#BKMer+Zs(8~4xk}%RKjs; zYpjcl{rAZrP#g`Yi>_3qnsu-!Y|)=2Wt__$ToB<^WopAz?NvB9uVrGH+` zD|6E8XgrCs-0;OQo+oI%#fRLDDAj4Af=@S7`y9TiR z18B;F67?*DVK*%Jw!Hi$mQyik`U(3$Kxe)ZKV&EqYA!3DKLBb&KVVb3D+NS6pTH() z3+NR_ZoIDk%0LiO?F~>Yd*!EZ1L}!LQA=1lp!@SjWl>5#@k`;_f8=54j^=n63Uh=shPVjNBCZ}z3OOZplpuk z7+s+(#J4?D2uu@o1_9{N0OW&^qzRy=IH90vhe5MsPN)?B05#P&Ns;CKPR{{28Ct#~ zL6>a_EGXFv>gHWO$dNJosmd5AdT@GZOo8I-TOf$!KivUkc)OCfK-<5xcjfpabxpM(5vq3RySN3Kfg5VS5FgAdtmEsi?*G9o8_Wm!l`}#{X$Qx z{`oSIR!Iw(z`)E9g{^ByUQ|<<0hLe0x_yyEfUPe;@j}x)xQWwFFFOQHq}lUc%{>K^ zf(h6OLopwl4|GFp)}jyIwM*l&x%$LZ`&R9IdN14*41kE(rVO9yk00S5_-7k|`sQ21 zj8XY%c>ne11He}xdK=aT2ygNV7U z*Wv)gW1{&8dea`5e@B)x9lL;^ODDjZhC8fH!B%$;67yx6f>hRQAPlNqgyISh#dQ_4 zJlneNXac+1yP_U_46ZC60d!msDxx({hU!8d37N5xqmR~vQVysc(}X&HzdG%|cdC8> z#K*OnrUFEM2StIO1R>#cG1`|se}Z&j+jBcf*#EKs1!tD z)ra}1V?C5OLF?ssj>mUQ5bFxfs2ied!FXb<9yci5WYJ@n_96LYqsNe93}5-(ybzT_ z$S(KQrU8r03ni4}@mI8@7M4J<2O9s|0Kt@dq0r@Gcw!I6rG7i8wyL&ignBF=y@=wd z0-{Jp625Rki@5jR^ADf%KJ_(rwe}z`$Lbvdj?a8SX;Mou>FKV06h0T6uO9L3p7eCn ze?VD~E_p#dWd2(7R&eFcV?x%!M^mdd^ffO`>6KQ2{wRu@o@NC9a))v1O|ecp;0q88 zvl7YceNe5yaQ99;8WWYUBrWI+uskJ0?HX`wn{xtKUUK0gtrat4D@McWjVScx4ep?q zHGEh6>x9r3qyZ#Bd9(rOuIrOz2t~6JN}cua;09q05EiEc^W?BilxjrB7T0b)ky8>& zI$*XYY;V$3jbC37)UhB`i?ow{4-CHHaRFmhOidveqcpX^Se+nA-OcYDV4YX9?U!<0 z&6;U;#l4W(ataLFWr~BbZilrt-UF_IwC0Gu-V{+aBG4s`rg?lPpbBGS=uAvHJ1f*4 zf9q`Fa6;zRpYi1?e7`e24**oR0UPMT7`6@P0pZ;TlgEH zsgl?MYCsifk%B=!`|3-w*+;J^g3IO_^9WM(lG0Z z)ptA3=?7QD60&0%IYvQ!>O7ONovcd808Nq3T$B z3rgA5P7K|MP~w=nZ%s_lu1z)g6|jrKm5VbT0fYX-_H<&npVEHlW@q&vQP-7=ysWEX zvWdF__FGhNFFV_r^}sANlvf^tgoHoVmT6WnEloUT_RHk#z-N%>tO>#04dG*mlA6GY zwd5qm#!R4I1#A$xOq-4=APSdB&U>L`fu=`vr>S{RkrPWZO>2=F^N-%-S`1SG4s)ae zC||NXRKH55^%yYjx61vlgq~9fr!oab<4)vZCgEWnw3eH|c>gBZRAjY9Yp3wMclCgkpuQV)B*Xnj728g90$LjX%kx=UvGl1FhNImGRP zuX9r`aTrG=zYAV+e=Y=WytWtaH1HNTj@@a1l_iV}qA{T~i9JSLI}$W8PHgn7wBe`} zIz|m`FgD$aCvWI^hGqu5s9#rXmJYl;mSYOBkbkTd_G<}M-aj$JK?$iYqoroK_4-y6#dPmsVt*r%aJ>0mX zUr1oZ`n0y|G@G)+=b3H>rhok(8_SHU=h^S9#_ui8T!^k}sQ)x;xpKJZ$zf5?5_!G# zhj(l@^XipnB((f&m;4d>!8#+7$Jo5MoMIn*yDfG#_?lmFbcP0Lfl1NVz2POEL`FOx zoxPjJeGGqQd4t3jjXOVd$4H0Gca!R>L3h-8=-aSz zj)UxbE6*0~{bABCPWlI(sNaG3e)@{HHUwNc-Z?N+H0y=fm(mp}`826}suBY~B*bJ1 zBJn%2cs+XF5rpHxQgMi3k8a*^kExD77}_*=+VGKxR6DGY|NHCAvFGX)DDk>r zBPq6(iCQ~J?eATquR2h2pZP$k5EZk#}!kP zFRS`N0%lU&z@ed9Cvg@>b?bAbH;%Eywmb`3;<2X%;EHYHZEnc9MJ0^cvh#BL)j5~O zKN3pqk-_yPT6?Cn^tkq+#cG&Jy`fLwAy1qWzfQQ&^2OxVVovc@t4YRTQt_3?UR7)A zsnrON|4$=xhwZpCrt7b9safvtE_)|_3IpxtrQhA2Yt?Ys4qxKt3SSwzW?ot@f3649 z6c)Q{M|9|uwX%~F@9}=5l3P%;^tJ7nhf9vk;1{YoiAm>`Gs-AyKjqZNE*^F7-#&x0G5JjS9560BTd zInaee_jB9i1a;A)H+hOvYFhhMG0FrtK7ClG>WW38!z8?&qQ#-a`l2u7~HELK~G~V zy-<4(efQpzSG6?Ozm$^}uBsAn$kB+G;U(4hPPVyK=XO(Y+kFh z)~9aHR)Q+l@wx?yn)Z%gNf^%Ncl*}22){;ZR#^LtfMBUtx1Gtme*Q@oZR`z-v(k@V z(psJ+&_8b?deHm)WP+R^w=(0jcSs(?3o9Za_pJGl&Yu3&lEdx$N45kc?(Wup5|;ee z7<^wpKaGp>M4`V&ls70%Lsrf;R?0i!@1J>*T)3hnFDL0Srt2nYR@^@sTaP|p(l6Bd ze1CaGQsJoUS2ojj{d>oflIt?Rk3?IrTWxw?{c;r5=@upr?r*Tgq!Al9&aE(36{a;# zv}MhfuxP3(C`Ls5P)2GoVODJ5d??CAn^_~Q=n+GGkw;C+h6_wJj@_msW2`JG=`3w2 zChG1Ma&2BZnZJsAN)7p4-wokuS=@D3@MRR)i(NeR{&~4WShBCL@o9Q1zoBb)cEbCtq$};Fyo1`*K%YpME~j$Gr#l8LU++mveVXXP zFN2w+`q0X{tZaLGLFrFvC4RxOdx6SNQz%uzV^gxFe#)y)_=A_D!tn@Op6B!;{*;u` z;nC;UuBYF4Gk&Vr+&0E`o^CGVF{MXHwzauL(bKNxuAu8Ir!WWIiwWCO4*0*NinHf) zPQTHvK$q|~s6_|y)f#yWXA$o&zrMEMC#!eJ@Ri*DTae|P|C_12j*~`HM4gNVm*zc& z&%3vM#mnGGrmMWvl)Dr}&MGlzyt(C7Bm51Wlr1Ev-P{@YbENq(?zEU6`p$VvO+US? zl{xAFo@mhJ@7tg!b}Y;n6I4f4@VH05*#BsRVO*i6SQL;r!Kn(&UaZ@VA$HW zl&T{RIiOrLBpKIf4A3mIo#b&}=HwA8iz`=BB+4-)Z5f zHVmXoCk#<35?sbLtmA#~Ehbi}hZWHtPjhKHD=8h`U?feRap(MK!e)Pn5cTG*Y6At0 z3`3aRYXlT$@#eS=Y@bjv`NU>*W^KyIQAK`GMVqkcqF(Qr7poUvRl%E;_Pco@s>R~+ z&-#-%G7&qS4Vt~j*z=)HOpIc?&q$bLG5X;NEwvO`n}#bIbR#czwz5Rfx;!=&?I_Cb zYw2;9dVUaz6u;0k&cqjt3h;5Fv&7tKyqIC_N^4`+y&s|9sqbE4IikbWZNMcbCft~r zG*=`%$bdu<9Y3Cm@#1Os(R1x9*P@#32MH7AYUbkQO;_c~?jZd%(ru{-cKp-IF>>a4 zcD)HzMo=4>_&d^2cV$*PZPSwdOA8=cG8c%9K_fizI|0P*1okBJ!Z)wD@+SUd1iKU| z&m)p4xWxV8b&Q|LZnH)k#i?>uyc&4h5r>Vam|dvng^7!gCMEtL>Y zBk_zoH?_UFr55E+`E`+-_}SZXS)~SZxA%>cIFk&^H{&a;pdFYL_vl~WevR?HsFziI zInLqN$f=JYx5RHc>16Q5XJ8?rVLw(_PxV5O8j4Lo)gtoQ0hCLVc{eM^_FHGTGZyzp^` z_?NJWDT2pV9aHum19%8eF*9}e+0Nvru#_LR((=YiL?=8y*$y)=T1wsK5!@ZNDXQ&i zQ=Jvriq1dxAf|+>B!uq zwS|kG_r0@>Vb!qAvEa2{d?ekRe$_&{5e2|0Evn1w9@lLn`#kFoA4l@e^H=I0w$f#I zJmEjb6!b{-fmBzolBgiIa4J?LMv2C=FVq|U48z=h9p0b&6JW6gb^0sZKbV)M2&W4A z`_5_8$WN>E(K>V8`)HyQU3}B8M)<^yGu-VokWp`d!z$48 zZfc%dbU7Vo|9Ro!jdy#=1N%3PUQ-D6WIQz9%n+@L&1t3eQt8dnn!)6D7A)IKr%co- zjZg4R-{QLhgN3U5-d*xByqxgCMmOaDz2uG+*LvPEPbppM%^|nZPVKzi)c=9Hb7F>=SGH?;N|C&q9OH#B}6;ZY20_C;V>3 zRSE2eu2r9dH+Bg&W{+F!o^iQ^d>~O^jNUoC$dBs}O5c+fkUt*O!9FTpCr318o*Ot8 z3pst4jW)%*o@><9aAZSK&a%yzlohazA72qnT5jQ;X5sYTx3K?st* z+{iT&&_q7w5&w;Lyd1drnS@GUElG|Rz90bA1gye{9}nVvC{^fUYDUVyQiQ`nubsy9 z0zw(Wb>_)AaeP%u=ExVww2eKf|NX-;AjCs$8R4J4H9;0E6_4G+sQOMmuX5z}A}9Lq zKZ>BSSwK9iazt7SgVtYcx*{0@W8NTobcgBBCphNbO|J8FcQ3*oLy-(GS==qYi#t!c z=J5Bg$G3yVD*wD8f&Uh8{$*O#r>mIBGuX>2CKpbTjIRyEqFnHm+H&YTi`T78{)eRg zho}EXs_=hatpRR9@#ppR^|}2Y|AD(31 zkZJJF`03xzHZT>ootmuweFTj}LCmw~iS(GqF)Y?TXA$>IxAL-3^50KvNU@?x&Epaf zZ{TY{z0?JF0M(uNpm{?m`v}g0})z1#yD7y-f;if>2ye6dpum>~VUDQA`oqWK~l=tBEcgG}kkx9LP zVKc*xiNC;rl0^i(V-*5U7aP0G${Rd4%@?Q^M7&J zqe)VV?;m1k|8nD3icSTt&zLPp&EfMLPhpSFx0e+V1@FHKK504@da|;4tJmo2b&0cP zKx(hJ{1>Th8KB`+*7ER}dwV45%J(@Wy%5xR>mx;d+QF^evV$UT&O(CcVRte+{BvhI zP0$soJrqNhsU6K1d+R+0_{~%)kffO|MsmQ zKNgyd+nrB86AIMngNCA86zL`kmqOqhC{lrbcI?*IDfH3-fz7$)3}i8?IadX{i@~j# zO24INoS|pI$tQO?1D%F<5_q*;SVQNB={bR~7SXR?OssxWd>7{(hrS)-h@Z2c?EtVs z`RfLB4)L|`b~0Ve;Cov;#hc^o3~!0hw4K2Cnzd0_H84~= z(3UX>GB=SZSh%n~;9pT6=8qpnbLUS$2?~^$h0I(b5s<+K2Jc+k9A_X0wR*2!r~W%o zS6mklz`B**s-~#awGR=Wg_8e)5{_O!gWcMldu2#}nCC}sVAgVi#tuowJPjgBVCgtd zy280Gbj2@J#h#?Go`+Al7-ahap@L(3|BDKq0^(1l-mTXNaYKTs&mvhK2fn|3X8S~t zD3mG}^vV>@6(Ua@;sS!b(!4Y;Q~r9=Bx`wsf`b{9MsUmANBp5v@D8;vm=eJSxDL6o z@#4_1Of>X4;MWz0_5?B2x+`MC7c%5U)i))DxB}y5k*cF2)-P;y#5HVTYO4ZW71G=t4HLp!?&Sh zEr&@n(6GsO@4#rjrV=OQ^OT%;1`rz!8N(GIk-h$ZTK9d78=>1oM<05RO%4K0e@Cbe zR{$s9>iccFkQA}8F1s%q85*`<8Z=L`1Zp>RCTUGhNDx~j&nkTsjizD6PX%d3vWuTk z^M=w!Bec(4z8`#Nd1&S8VZ8S@l1^yMdf^a1wK5@M=NYhOl4D-`sbs30<}S7zTZm!; zx(5QwYxZCfd_iwo-76joLK5he7ZC6C8(fC_-1mZa%@vmtnAwE<=WL@Cz&nZmljxQO z4BRL28VAz6&O8)6q~~Yv0^@%G!HEkL5;LcN{`jUBR$g9C98%LAVTW55;XY8V3!Jc} z^7CGy8wHr7OO2OcN&u19AFAu%}{;LHa9|2!Z?z4*iwcp0z7I3Q-xwD z@;?0#O%;7P1*xGnBZV40k5EK!*%RfIm@NPGzH47nx5Rv`{xKbx`p#{pMSg||0;Q=U zHAN180DD<5GZ26I?5!suXUakTLgeviP%lxQpF>Yaeh1TInJUC$$DFYp(IQ_7H1Iko zqvXJNeeJ>@UNx0U+Ua(NH>cdnM7Pk8^LMT6{CdM#a6+0mmlSoAG{ZB0v^N+VcOpH^ zt?^D(7qn7^`7^f?k)5|zdumiE-S~@e)u3$f8{V|Hj=$0RJHxwU^1yrU&k7ZADoPe| zDfRKp%y~F(36GB=glh!!hvtjET>O?hLD<3%TzcHKXZc`oh&7V+1=}C)Q?fT$I)G+p zp@wfWVRDa|Nt1$QPwh8MFv8mxs3}agNLOJNoiAqQwZ-AIru@NFJ(_EMG7ap9(SeS5 zf@Qo^5)HCh~j8}!sKa{|- zuJ5p{>tdfOQJVREu7QXXJWT$nFYt_Udk+#bu$qEif-82BX$?K6KISq6d*hfKyhDau z@1c%K4lINE=R&+v2aq(}_qGWUBn>cg-AlGc zdV9AX?pE5iWz!YW*{ND&9ef*BWA3jHx0btY+Vp~jgD`3QE2!tLZNWI)%$5K79mGMx zdDpmDS+#UY@;1jgg6URNK4+aqw1LP@%v$PX8vTW)22*M(g*1O_q(02A+1-!u%WcOH7ZracZ?ub@p%bov0n;ch+55r0{ zO5&2)$eSJ<^N)+x&Uo4#YQWyyvjE9fqjsOtJiA(VY6yOfdF&DihZ50T;>%`u+QW1O z-MoHh;ZWJX{SI@Ml5-CG+)pVx9u47p_i|-5yp^~;6>KXOM99wUn<^P(5u-I@iQB6_ z50OnM7c_&uY}T4yM6yh3i;F>B9=CTfviAp4hiT%x8)scgUhmi>yPM(|K3~RYV7s1F zyz|2j(cyq34oFk{NNt{rks0h(7N@$-CO7FY5jW0m@RexDplMFJ%$0KKH(YQpP0|)BFM5trK2wefb5p-Qk&KHZiny24&CI zxBGVZ*ELn8QrjxL1msm{nt?_+! zoPaNWGLAjIa?>^SpLg3>i@p3%uJk3&GVO5@1JcLbqwJ5J=Z@N1q-sEC&ytZgRS`cThn6egxablD{txU6e+Ettmxl+>z>3pRukr*yWbO-kyJH1eD9HJ zd`w{rc2C|-(~TZQjL-yj-FV(wWZPn1B8hmkQiXb*ZXz9cUM*oN6*OFGGTW81X&fFK z5wh1)S`Cc$()E>G3BE-Stn+EhQU6dzn`XcY`A_8D?Uwz~`m%7S~?=9_v9R{+6C5P!Ih)^G7b4UG6lJ0>9 zn3F0^EvW5@&Uya;l!jM*d{T0pEN>0(k=3V8k%Z_^Iju1jUS22+`${IpM9H$Qn<9d@ zHe-tKrMtTzi7#g)?y{>&WqPp)PZq1wzN{VP*085?mj!EA<&GB}qpE2}J09cu(74@@ z3C~%Q#I{AePXce~vxq}q3g3ypzf-L&612f_O8ciQ(+N87uUz(V(zm_ri|t90^fe~P zJ83`Z7-S7Qqol99G>yi|5=snt$Q*gL_7sPQ6Xn^=N=M_XHbfHkt>@VkFc}Z0I=DrT zAKUtf)d|~PV!^DMJJ>YJ^|bmPzNWyF@%?(X*JGRbQ3_oK!{Hk*H7(KRE*9E{%0$T` z*TOVYShKE^xp4d+Ku-O7?;KiW_d&3PPta>!O0r=T{}k-JA$(zzCcddGg@WH+g>|bNuCsb6)+I z{Ow0$!kUQQI98<$~OsF`Xe-$>uQ3H$DlALxT0CGso_9T>?5jSH=+@%rtFpRn6@*-)Q3jH zw$#Y;_!7p^MbGt3%}>|g{O+>mXWj8qZ>AN|5xhGs z`<(mD^q2H}DnYZTww;$`Y7>$5Jgk&vo4lS*H(qGyrMx${MXY~vxX7u{6s^%br^ z6(-N+yPv~P8ZH~NUGK9pi;#;96CF-n*-P#(WICK7VZP9;Z_&`5;K#*EgV+Adpi6T! zhd9%8f$Koi$7(V@fDabz*J+@q{a{NQ^K8@6gsEDKAP|(7@r3AmsWw(Iwiz5YmVSdA z42x6IZ!yPJqEH-Oy66V#zI_r!=L_rEH*iqY7vpgw>TXUs4a|w4XoRwvTWp4UNKdut z;joGPX!=}1*qTA%zmU+zaKerxsuckh)2W~IlAH@oipnC7VUmiz$t3s6Cd6M|S(k;d zb3JJu_JK9Q=6}A*O9h+Ies$Y#T7H{vQ5-fymGWck^ZK#p3J57i~2=gvr%j zKHXwaeUpQZZS09!ArzrJCI3!%P;}MwtDoJLwaheT`xQqwYjm#V2`7Cnw?fiEkx_Yj zO}nq(ml_o@wHJf)iP3CgQy==SV{DVqZ|{wBW|}LtN)hoC8=9`f3UY4iS_G^X+1^CU zgqq!j_ka19#3$RKY>lUqPAtUYd~_WXWy)Pzk7YIGSW?y?;2_-2Z!GO08KPX4fA2hP zc!E6&O`KXLGE;X=_u>8a1P0J!D9|kmYbI=3PC4j2HDOxgChqT`KP%}J-awt2k@R-f zwmJ;QT*1n+M<(@~ci3u-cces*XTY9kZs4Jb2l70CUjEqSP=Aj3Si(B0Sp~^Ve2rmI z?^DT9XwG>f?XDG3717IyM@Mp}D+k`JjKykS;v(1xV{1E^dQUBaQ9J0LqFK#q4&}Yadf`YnFzx zet$YePN{hCVZ1i3%D(V3svFjMK705z;^=d-ZN|H*)49tlxu^bXh%6Bz1>m$@O|Z_7 z7$!k2+L$y-$N9=z>ihh34t+z(P**#&pNr%d?MG0Br1VCR06tpmDJmeQW?`$Iz%^6 zd4E7MuoZOTS;48O;VT1s)}rgxL0-z!H|mB}<<_I(q+f8k46>(W5wK%&$eUiQv$?kE zRXEwGSmwn}1EW`gK4Bx>c&uqC7boBsW}v66zvrbeOG1lyPr%6gw_UU%s$Joa`3>UK zxINEh(P{aGFDLn0VMN)7?-F$4LU4ibnr+@bv7X?AH@bz1Qg>;D*0FZ15EfzAi7L>D zi8+(;&|UA_)os(K{d6gpvqku`$k9=>^})>YjO_S+RIT|ZI}|%dqlK0nr>@9pHi^Zl z1qxoM1x2MJ9;f*DU-AlCd+*DnK~N;)n2AH4hl1Ct#FhF*#8ayOwRg~y=%kGz==aewF+P~(eVEwEi9{mT51Z{Vi;@}9MI{BnFsuILFnzMoS8 zPmm9H0S4~83t(tLN^-i$9~I$)T75T;Y$2zJRQgNZs3>ndL#n^0mgswjeCBujY7|A; zzTb-Sh5N85rg*uc_bE-T*L$$dXe8#r1o%S_7|Fm~=kF&RC&j)^W4WJfrGiGNn@5Vz zzxb*QXZA!jq%LIuBWvO~pTPFiw8q1`(@#6=tC;kHL#?k}&hKNUTPqzl2>N8&a7>SJ zHofbjOm!{C)Mibvy7e#4FSKy5=BTs3ar=%A4Xqz~= zy;cqdt_Kd?0$4`?0U<5md*KR<71*!yp`y+M4Pn#y*8$qtLGpJ$sHq)?erFe9aEI$3 zMmpI2-LGI@L!WUx*rA7nqMo|kcmxWCtGSE=iC6Zr?T(K&rqq#F6hkCc4HQoPIY=5i z8Jm-|L|Tp8Lh8WVX=tQq@E!FpyDT2gvuDqe+l1F1EB*Py2m)}m^4eqVx5rPV;Pj|x zm&T2{6#irE{Kwn+kG=Q*yu%rK7xn{CE8yUbRZ)Q1un#9#@4)`Y*nHZ z;|@LXAde(Z&fhEj+0zI8bG@pA`SLGreaUW}gH8qzaTSrBoqb^RM>DI>V)<%s!2cSxtFCx6^Qzh|Pt)*%J}c+xC#Y2MykQ(E-NCMzjawZAq zx4^#umPn`vVK>Ctj?Jb=Nv@gLGM)9f&C}{GC56$vW|n(txB{ z5EC`7l|Gfs&N}>;^)uol1YSrr|f5!xC6O6m>N)(T#W-SE$Ssd!vp zW$ePBO4tm+PfyEg2)4i70E7iRYwP^A7=s%|U`}WU>chmL8f;UU^oSKqW;~;I=v6kA z;>d*Qz?&Y#?Hob?3xt_XoMejehTf}EeZA%*t?y}sg+({ElyGrt#_Jc&K^7) zw_v#HNeqtAq?REi!keFlLimvo$OEX|JZxwdvQ7Hxtn*}j?WJ2qC#0FjII)AM2(Lp} z21M!t0I)(6y7%CjN>adA&NE`Xy)*n7qTHZsR|l6l;X1@D1aT~vssX#x!lB}nCh}_*AvGxjX9K{aeU#_-5!|Bv zAM!_jAd!?ukTa!CM=o`aqK{iIKL}isNe1_!S0Xgq3jzJxC%}Y>m*^aC0dYd}zW&_R zkHFA?hao`aJDyhe`6ED|uXNKobd7>#I{~BqmrIl5xYnEJ*bE9qGO!dq`^>^E(MGFPrFn!7EkLy4BCCTeZ>RT(EX7aHX>lPHfD^_R6%+d$UUh=YU9EWUjAP8~m%r{>dVOir^O83pE#5}3b>xGLoG z;6~ks38Q*SJ)7!ShlTUYV5&37RAL&X*r?&~v#F26g6Qo-)Q+5nP74h12gyWj*^q{b zrq8nUr)6Zk95dXC7$AGDs^NKMg9nqrrLn0`pPv`WVr=Z{t2*~{4tOYI9)BPWp-12^ zpg2w38<(Ekf(*ja?ORrCqu>PWy_(dxoE_V|0Yf31YDM@-JAP1EMWJSQi}C3P&qz1<{Qrsd=i+FPmW8_H6K9cnuk;mbn|3fo16bM7s;%9 z{GL59Ucl%{WKgy>=tKs0#_8xzyHZKwU)}jaeg^DuDwFvb@g`OrY z)}9Cg`cWES3voJE35bF9c9Qm2AY-)ZQu9{t01K|=0c2j{Z{D*k@qlr|J!`NG1afg3 z*^l^g5grCJhM(goq*g1S3R#OPtOGWWbg4~jGgB5FwJdirGHTF^y(GCVP2pr8GJ^@| z1O4Wmr^BWeG<5Ow7cg_0=M?b;w&8OR9Tk4ongg*xdIyi#DFv_u#?chFYuig3w{7sq zJcW9b)ub8hm|r0T33?qe{+<9@Eo{>IM*KcYW&SRm|9ir5VO+*uWaJV{ZqIBhAkkHN9I zBu5;#2Yvl{ePDRDcoH%oSK=+Yu=9#Ja9!jK((;wni6i_u@SGB3*FrH8KiF%}79pv- zK+i6JK4sIv(@rau0d$hFsS~H1hX2Z0+6O-hQ+08iVO%^`r1kF)-voB>UJ3{Q;pTms zaSSQUd>+Tda|r@>*E&aYu-4ZHH{tEqJs|f#$wG|cPVnLLlD-jp{t`q z`}3f@@}_>Oga;qdRh}Nl+A96Dk}Ix7cka=jc)727@sTx)WWD51`ntT(HnDhnUkeQ>hgZD!%0A%*e-VNNE1e|z`ah86JjsiODBK5le$U} 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 literal 0 HcmV?d00001 diff --git a/addons/2064123047/icons/frozen.png b/addons/2064123047/icons/frozen.png new file mode 100755 index 0000000000000000000000000000000000000000..83fc7d28773064e8c0cb81426887a5f10877eda4 GIT binary patch literal 1111 zcmV-d1gQIoP)^s+ zUBgA33>cTe1PUr)pkp_YA1?c&(abTRcfNkCh5GgJJ$w?Qz;sLf@gyhbeRH1kp7WgZ zN&rayPyF|bD%>8LZA+3W?E56DUqXUT5TSIWZLbi76hi>(R4!dm3D&8dMX`GC&j;!O zLV{s-JWAs#2nvDZz;6#E7`@iKWVr2$z=GW^*<)R?CeOb0sX}+L zG}jSM{+D#_uhngWaQI7rpl%lg^3mB!)#O960!MzN(z=QIBuT$`=aK~XXH3eX3;3~3 zrCwXA$v>k!4!o}C+UGAxaNFie?qux%KzNhctSizbd$93SzK^n}j0_f_;k1}>-Dv@>XMv7$BHEc< z&wNea0mp`b>V81mDZb)0-Hc_~Ca>2xOH$3VT)dGVjWtYnSN8ut*UW;nv4AJf1E(&E z=)543w44D<-w1yBQI93bJoDq8dcRLr@b?*6EUOQvR!Y*Z-9vVCsY9;r^j&RdS$DC( zJ)=PH4JUJ80In~9Kv*CK96@TA(Ez2oStTV6dg7H6Ec7`_txROi+aF=HwXp@Wr+ zdP3zh4+w#6)!b0B`TFJEb^lVL4JCXJDS;GG-Xm(hahNrEP(aIw*b4$Fsh0Ujw)KNR zWiL?NArkhBT z1o>Zsc1N7iGpuQ4Gw=nlw+7g009NO^@)i+K_9^5AGwYM|I=p^8g|H<@Mx|x>z|)W dPzY`ne*kBNVB*gAG&cYM002ovPDHLkV1hcr9|r&c literal 0 HcmV?d00001 diff --git a/addons/2064123047/icons/unfrozen.png b/addons/2064123047/icons/unfrozen.png new file mode 100755 index 0000000000000000000000000000000000000000..53aa2c29b1cc1429a7ebcf3023bbf34b3936dbe2 GIT binary patch literal 969 zcmV;)12+7LP)o~p|pW~?GIOA&^ z6&Hn@+C)MbWC%edSb(f zY)o))@I}7^#Kpyhm6w;hD=RA{IXPMHSAdk1lp>u@_pzm=MQ9Zj729cPY59Hu2n!3l ziiA5oJw4Lf+p8G;s;a7;prD{@Cjtlz47>!pl8}(_EF&XBo0yn*oAJE7yqUhfKKIDT zh|r8iqnk0FB_$1JVJ;kTNanx9==U82=$hm=by z)tQo#lD9K6GeV=!7|(FW$QM;tSML@T6@5Yu7sT-32tZ6sOfsG=&CSiy+1V-m{ryr} zTIxVm7kG{Y2D{xZH2NN8fj;ZNz<_jjcPstDnLuet-T(q}b93LfwY7=CU=WkZB$Jbq z(%9Izk(HGdQCnNZ+Y!Oet!NG zt7kXr=wOVCqX-SrsmldPWn8V=^o6RO>vss3QhLl$Tzbw%RZz#s&umx6y}CkDM{#_dLx%79b=fB!E&qY+^8zuv%8_a=8?v zPk`a!;V+;hnn^hm0J-ZE028amQ-5F`Y=RwX$Lwv~EjJPY=b}3X_8>Wrei1whDIE4(8zT5rFvk_-t%TCKS&s z==FN1r{PeFfs&-rKkU&t4kOlxbaZqGC=za4Y+jAG7u-h=Y?+yv8@RMbsxbOfo{2?e zCe8*2i#}sKLz73C9232Z;ai2&rvPxr=QhP_o?{(rJv#-9&59v9;QBaR3kQrH-o1&W z8Z5@Ar&`yPAE^3D+E^)#uLCvTPrR;u{BW{C*N6K0dZkAMMC^@Uf0
{1}
".format(ord, fieldContent, nbCol); +} + +function createNameTd(ord, fieldName, nbColThisField, nbColTotal, sticky, imgFrozen, imgUnfrozen){ + img = sticky?imgFrozen:imgUnfrozen; + title =(sticky?"Unf":"F")+"reeze field "+fieldName; + txt = "{1}".format(nbColThisField, fieldName); + if (nbColTotal>1){ + txt+= "".format(ord); + } + txt+="".format(title, img, ord); + //console.log("img is "+img); + return txt; +} + +function setFieldsMC(fields, nbCol, imgFrozen, imgUnfrozen) { + //console.log("Set fields with "+nbCol+" columns") + /*Replace #fields by the HTML to show the list of fields to edit. + Potentially change buttons + + fields -- a list of fields, as (name of the field, current value, whether it has its own line) + nbCol -- number of colum*/ + var txt = ""; + var width = 100/nbCol; + var partialNames = ""; + var partialFields = ""; + var lengthLine = 0; + for (var i = 0; i < fields.length; i++) { + var fieldName = fields[i][0]; + var fieldContent = fields[i][1]; + var alone = fields[i][2]; + var sticky = fields[i][3]; + if (!fieldContent) { + fieldContent = "
"; + } + //console.log("fieldName: "+fieldName+", fieldContent: "+fieldContent+", alone: "+alone); + nbColThisField = (alone)?nbCol:1; + fieldContentHtml = createDiv(i, fieldContent, nbColThisField); + fieldNameHtml = createNameTd(i, fieldName, nbColThisField, nbCol, sticky, imgFrozen, imgUnfrozen) + if (alone){ + nameTd = fieldNameHtml + txt += ""+fieldNameHtml+""+fieldContentHtml+""; + }else{ + lengthLine++; + partialNames += fieldNameHtml + partialFields += fieldContentHtml + } + //When a line is full, or last field, append it to txt. + if (lengthLine == nbCol || ( i == fields.length -1 && lengthLine>0)){ + txt+= ""+partialNames+""; + partialNames = ""; + txt+= ""+partialFields+""; + partialFields = ""; + lengthLine = 0; + } + } + $("#fields").html("" + txt + "
"); + maybeDisableButtons(); +} diff --git a/addons/2064123047/meta.json b/addons/2064123047/meta.json new file mode 100644 index 00000000000..b2aef2247da --- /dev/null +++ b/addons/2064123047/meta.json @@ -0,0 +1 @@ +{"name": "Advanced note editor Multi-column Frozen fields", "mod": 1561905302, "disabled": false, "config": {"MAX_COLUMNS": 18, "same config for each window": true, "Frozen Fields number": "516643804", "mids": {"1548452101015": {"count": 2, "Front": true}, "1548452101021": {}, "1502402408169": {"count": 3}, "1500687608976": {"count": 4}, "1500296807433": {"count": 3}, "1510327840149": {"count": 3, "Smaller to greater": true, "Variables": true, "Smaller0": true, "Greater0": true}, "1517190992616": {"count": 2}, "1541341382854": {"count": 3}, "1503607608923": {"count": 3, "Name": true, "Notation": false, "Variables": true, "Definition": true, "Definition2": true}, "1503151380077": {}, "1538428339263": {"count": 3}, "1526240760793": {}, "1516973990716": {"count": 5, "Extra": true, "Program": true}, "1523883416003": {"count": 3}, "1503661491772": {"count": 3, "Answer": true, "Question": true, "Variables": true, "Answer2": true, "Construction": false}, "1520321531022": {"count": 3}, "1548466114993": {"count": 2}, "1512522399704": {"count": 3}, "1500637447936": {"count": 3}, "1549332792078": {"count": 3, "Effect": true, "Instruction": true, "Abbreviation": false, "Returns": true, "Implementation": true, "Variables": true, "Meaning": true}, "1516974248815": {}, "1549864636843": {}, "1537023184268": {}, "1516979344387": {"count": 3}, "1500422819013": {"count": 3, "Variance computation": true, "Mean computation": true, "Cumulative function computation": true, "Skewness computation": true, "MGF computation": true, "CF computation": true, "Nth moment computation": true, "Kurtosis computation": true, "Nth central moment computation": true, "Characteristic function computation": true, "Mode computation": true, "Median computation": true, "Moment generating function computation": true, "Nth cumulant computation": true}, "1533927692530": {"count": 2}, "1531060262909": {"count": 3, "Definition7": false, "Definition": true}, "1453825185007": {}, "1486866910751": {"count": 6, "Variables": true, "Text": true}, "1500150282671": {"count": 3}, "1545642070762": {"count": 3}, "1536761954139": {}, "1503064370037": {"count": 3, "Name": true, "Definition": true}, "1342704248562": {}, "1499533524294": {}, "1342699478189": {}, "1538958357994": {"count": 5}, "1550705322075": {"count": 2}, "1502744315690": {"count": 3}, "1499799667645": {"count": 2, "Result": false}, "1548525134849": {"count": 4, "Definition": true, "Variables": true}, "1550450287057": {"count": 7}, "1550225260587": {}, "1550422260357": {"count": 3, "Name": true, "Definition": true}, "1499289596284": {}, "1531856334158": {"count": 4}, "1541285845376": {}, "1552537148872": {"count": 1}, "1521035478471": {"count": 3}, "1521035478470": {}, "1468476637548": {"count": 3, "First name": false, "Last name": true, "Role": true}, "1500748443689": {}, "1500637447937": {"count": 1}}}} \ No newline at end of file diff --git a/addons/2064123047/multiColumnEdit.py b/addons/2064123047/multiColumnEdit.py new file mode 100755 index 00000000000..3701cd73dab --- /dev/null +++ b/addons/2064123047/multiColumnEdit.py @@ -0,0 +1,211 @@ +from anki.lang import _ +from aqt import editor +from aqt.editor import * +from aqt.editor import _html +from aqt.qt import * +from aqt.utils import shortcut +import os +import json +from anki.hooks import runHook + +old_init = Editor.__init__ + +def __init__(self, *args, **kwargs): + self.modelChanged = False + self.model = None + old_init(self, *args, **kwargs) +Editor.__init__ = __init__ + +old_setupTags = Editor.setupTags +def setupTags(self, *args, **kwargs): + g = QGroupBox(self.widget) + g.setFlat(True) + tabLine = QHBoxLayout() + tabLine.setSpacing(12) + tabLine.setContentsMargins(6,6,6,6) + + self.setupActualTags(tabLine) + self.setupNumberColum(tabLine) + + self.outerLayout.addWidget(g) + g.setLayout(tabLine) +Editor.setupTags = setupTags + +def setupActualTags(self, tabLine): + import aqt.tagedit + # tags + tagLabel = QLabel(_("Tags")) + tabLine.addWidget(tagLabel) + self.tags = aqt.tagedit.TagEdit(self.widget) + self.tags.lostFocus.connect(self.saveTags) + self.tags.setToolTip(shortcut(_("Jump to tags with Ctrl+Shift+T"))) + tabLine.addWidget(self.tags) +Editor.setupActualTags = setupActualTags + +def setupNumberColum(self, tabLine): + label = QLabel("Columns:", self.widget) + tabLine.addWidget(label) + self.ccSpin = QSpinBox(self.widget) + tabLine.addWidget(self.ccSpin) + self.ccSpin.setMinimum(1) + self.ccSpin.valueChanged.connect(lambda value: self.onColumnCountChanged(value)) +Editor.setupNumberColum = setupNumberColum + +oldOnBridgeCmd = Editor.onBridgeCmd +def onBridgeCmd(self, cmd): + if cmd.startswith("toggleFroze"): + fieldNumber = cmd.split(":", 1)[1] + fieldNumber = int(fieldNumber) + fieldObject = self.model['flds'][fieldNumber] + self.modelChanged = True + fieldObject["sticky"] = not fieldObject.get("sticky", False) + self.loadNote() + + elif cmd.startswith("toggleLineAlone"): + fieldNumber = cmd.split(":", 1)[1] + fieldNumber = int(fieldNumber) + fieldObject = self.model['flds'][fieldNumber] + fieldObject["Line alone"] = not fieldObject.get("Line alone", False) + self.modelChanged = True + self.loadNote() + else: + oldOnBridgeCmd(self, cmd) +Editor.onBridgeCmd = onBridgeCmd + +oldSetNote = Editor.setNote +def setNote(self, note, hide=True, focusTo=None): + if note: + self.model = note.model() + else: + self.model = None + oldSetNote(self, note, hide=True, focusTo=None) + if self.modelChanged: + self.mw.col.models.save(self.model) + self.modelChanged = False + if self.note: + self.ccSpin.setValue(self.model.get("number of columns", 1)) +Editor.setNote = setNote + +def loadNote(self, focusTo=None): + """Todo + + focusTo -- Whether focus should be set to some field.""" + if not self.note: + return + + # Triple, for each fields, with (field name, field + # content modified so that it's image's url can be + # used locally, and whether it is on its own line) + data = [] + for ord, (fld, val) in enumerate(self.note.items()): + val = self.mw.col.media.escapeImages(val) + field = self.model["flds"][ord] + lineAlone = field.get("Line alone", False) + sticky = field.get("sticky", False) + data.append((fld, val, lineAlone, sticky)) + self.widget.show() + self.updateTags() + + def oncallback(arg): + if not self.note: + return + self.setupForegroundButton() + self.checkValid() + if focusTo is not None: + self.web.setFocus() + runHook("loadNote", self) + + self.web.evalWithCallback("setFieldsMC(%s, %d, '%s', '%s'); setFonts(%s); focusField(%s); setNoteId(%s)" % ( + json.dumps(data), + self.model.get("number of columns", 1), + self.resourceToData(icon_path_frozen), + self.resourceToData(icon_path_unfrozen), + json.dumps(self.fonts()), + json.dumps(focusTo), + json.dumps(self.note.id)), + oncallback) +Editor.loadNote = loadNote + + +def onColumnCountChanged(self, count): + "Save column count to settings and re-draw with new count." + self.model["number of columns"] = count + self.modelChanged = True + self.loadNote() +Editor.onColumnCountChanged = onColumnCountChanged + +__location__ = os.path.realpath( + os.path.join(os.getcwd(), os.path.dirname(__file__))) +js_file = os.path.join(__location__,"js.js") +user_files = os.path.join(__location__,"user_files") +css_file = os.path.join(user_files,"css.css") +icon_path = os.path.join(__location__,"icons") +icon_path_frozen = os.path.join(icon_path, "frozen.png") +icon_path_unfrozen = os.path.join(icon_path, "unfrozen.png") +with open(js_file,"r") as f: + js= f.read() +with open(css_file,"r") as f: + css= f.read() + +js = f"""""" +css = f"""""" + + +def setupWeb(self): + self.web = EditorWebView(self.widget, self) + self.web.title = "editor" + self.web.allowDrops = True + self.web.onBridgeCmd = self.onBridgeCmd + self.outerLayout.addWidget(self.web, 1) + + # List of buttons on top right of editor + righttopbtns = list() + righttopbtns.append(self._addButton('text_bold', 'bold', _("Bold text (Ctrl+B)"), id='bold')) + righttopbtns.append(self._addButton('text_italic', 'italic', _("Italic text (Ctrl+I)"), id='italic')) + righttopbtns.append(self._addButton('text_under', 'underline', _("Underline text (Ctrl+U)"), id='underline')) + righttopbtns.append(self._addButton('text_super', 'super', _("Superscript (Ctrl++)"), id='superscript')) + righttopbtns.append(self._addButton('text_sub', 'sub', _("Subscript (Ctrl+=)"), id='subscript')) + righttopbtns.append(self._addButton('text_clear', 'clear', _("Remove formatting (Ctrl+R)"))) + # The color selection buttons do not use an icon so the HTML must be specified manually + tip = _("Set foreground colour (F7)") + righttopbtns.append(''''''.format(tip)) + tip = _("Change colour (F8)") + righttopbtns.append(''''''.format(tip)) + righttopbtns.append(self._addButton('text_cloze', 'cloze', _("Cloze deletion (Ctrl+Shift+C)"))) + righttopbtns.append(self._addButton('paperclip', 'attach', _("Attach pictures/audio/video (F3)"))) + righttopbtns.append(self._addButton('media-record', 'record', _("Record audio (F5)"))) + righttopbtns.append(self._addButton('more', 'more')) + righttopbtns = runFilter("setupEditorButtons", righttopbtns, self) + + # Fields... and Cards... button on top lefts, and + lefttopbtns = """ + + + """%dict(flds=_("Fields"), cards=_("Cards"), + fldsTitle=_("Customize Fields"), + cardsTitle=shortcut(_("Customize Card Templates (Ctrl+L)"))) + topbuts= """ +
+ %(lefttopbtns)s +
+
+ %(rightbts)s +
+ """ % dict(lefttopbtns = lefttopbtns, rightbts="".join(righttopbtns)) + bgcol = self.mw.app.palette().window().color().name() + # then load page + html = _html % ( + bgcol, bgcol, + topbuts, + _("Show Duplicates")) + self.web.stdHtml(html, + css=["editor.css"], # only difference, css and js file + js=["jquery.js", "editor.js"], + head=js+css) +Editor.setupWeb = setupWeb diff --git a/addons/2064123047/user_files/css.css b/addons/2064123047/user_files/css.css new file mode 100755 index 00000000000..846ccbb00bf --- /dev/null +++ b/addons/2064123047/user_files/css.css @@ -0,0 +1,7 @@ +.mceTable { table-layout: fixed; height: 1%%; width: 100%%;} +.mceTable td .field { height: 100%%; } + + /* Apply fixed width to first , which is a Frozen Fields cell, + or it will take up too much space. From makeColumns2*/ +/*#fields td:first-child{width:28px}*/ +.fixedField{width:28px} diff --git a/addons/2064123047/zipping b/addons/2064123047/zipping new file mode 100755 index 00000000000..9a72c1f89d7 --- /dev/null +++ b/addons/2064123047/zipping @@ -0,0 +1,2 @@ +rm mcne.zip +zip -r mcne.zip *js icons user_files *py zipping *md user_files LICENSE *json --exclude \*~ \ No newline at end of file diff --git a/addons/3491767031/__init__.py b/addons/3491767031/__init__.py new file mode 100644 index 00000000000..002559b3d43 --- /dev/null +++ b/addons/3491767031/__init__.py @@ -0,0 +1,257 @@ +# -*- coding: utf-8 -*- +# Version: 2.5 +# See github page to report issues or to contribute: +# https://github.com/hssm/anki-addons + +from anki.hooks import wrap +from aqt import * +from aqt.editor import Editor +import aqt.editor + +# A sensible maximum number of columns we are able to set +MAX_COLUMNS = 18 + +# Settings key to remember column count +CONF_KEY_COLUMN_COUNT = 'multi_column_count' + +# Flag to enable hack to make Frozen Fields look normal +ffFix = False + +aqt.editor._html += """ + +""" + +def getKeyForContext(self): + """Get a key that takes into account the parent window type and + the note type. + + This allows us to have a different key for different contexts, + since we may want different column counts in the browser vs + note adder, or for different note types. + """ + return "%s-%s-%s" % (CONF_KEY_COLUMN_COUNT, + self.parentWindow.__class__.__name__, + self.note.mid) + + +def onColumnCountChanged(self, count): + "Save column count to settings and re-draw with new count." + mw.pm.profile[getKeyForContext(self)] = count + self.loadNote() + + +def myEditorInit(self, mw, widget, parentWindow, addMode=False): + self.ccSpin = QSpinBox(self.widget) + b = QPushButton(u"▾") + b.clicked.connect(lambda: onConfigClick(self)) + b.setFixedHeight(self.tags.height()) + b.setFixedWidth(25) + b.setAutoDefault(False) + hbox = QHBoxLayout() + hbox.setSpacing(0) + label = QLabel("Columns:", self.widget) + hbox.addWidget(label) + hbox.addWidget(self.ccSpin) + hbox.addWidget(b) + + self.ccSpin.setMinimum(1) + self.ccSpin.setMaximum(MAX_COLUMNS) + self.ccSpin.valueChanged.connect(lambda value: onColumnCountChanged(self, value)) + + # We will place the column count editor next to the tags widget. + pLayout = self.tags.parentWidget().layout() + # Get the indices of the tags widget + (rIdx, cIdx, r, c) = pLayout.getItemPosition(pLayout.indexOf(self.tags)) + # Place ours on the same row, to its right. + pLayout.addLayout(hbox, rIdx, cIdx+1) + + # If the user has the Frozen Fields add-on installed, tweak the + # layout a bit to make it look right. + global ffFix + try: + __import__("Frozen Fields") + ffFix = True + except: + pass + + +def myOnBridgeCmd(self, cmd): + """ + Called from JavaScript to inject some values before it needs + them. + """ + if cmd == "mceTrigger": + count = mw.pm.profile.get(getKeyForContext(self), 1) + self.web.eval("setColumnCount(%d);" % count) + self.ccSpin.blockSignals(True) + self.ccSpin.setValue(count) + self.ccSpin.blockSignals(False) + for fld, val in self.note.items(): + if mw.pm.profile.get(getKeyForContext(self)+fld, False): + self.web.eval("setSingleLine('%s');" % fld) + if ffFix: + self.web.eval("setFFFix(true)") + self.web.eval("makeColumns2()") + + +def onConfigClick(self): + m = QMenu(self.mw) + def addCheckableAction(menu, key, text): + a = menu.addAction(text) + a.setCheckable(True) + a.setChecked(mw.pm.profile.get(key, False)) + a.toggled.connect(lambda b, k=key: onCheck(self, k)) + + # Descriptive title thing + a = QAction(u"―Single Row―", m) + a.setEnabled(False) + m.addAction(a) + + for fld, val in self.note.items(): + key = getKeyForContext(self) + fld + addCheckableAction(m, key, fld) + + m.exec_(QCursor.pos()) + + +def onCheck(self, key): + mw.pm.profile[key] = not mw.pm.profile.get(key) + self.loadNote() + + +Editor.__init__ = wrap(Editor.__init__, myEditorInit) +Editor.onBridgeCmd = wrap(Editor.onBridgeCmd, myOnBridgeCmd, 'before') diff --git a/addons/3491767031/meta.json b/addons/3491767031/meta.json new file mode 100644 index 00000000000..6227e5f5c0a --- /dev/null +++ b/addons/3491767031/meta.json @@ -0,0 +1 @@ +{"name": "Multi-column note editor", "mod": 1560844854} \ No newline at end of file diff --git a/anki/collection.py b/anki/collection.py index 691686eb4c9..00586447735 100644 --- a/anki/collection.py +++ b/anki/collection.py @@ -1000,7 +1000,6 @@ def fixIntegrity(self): ret = self.integrity()#If the database itself is broken, there is nothing else to do. if ret: return ret - for f in self.listFix:#execute all methods required to fix something getattr(self,f)() # whether sqlite find a problem in its database diff --git a/aqt/addons.py b/aqt/addons.py index 3d76dacec77..c08b4c1c144 100644 --- a/aqt/addons.py +++ b/aqt/addons.py @@ -903,6 +903,8 @@ def __hash__(self): incorporatedAddonsSet = { Addon("Adding note and changing note type become quicker", 802285486, gitHash = "f1b2df03f4040e7820454052a2088a7672d819b2", gitRepo = "https://github.com/Arthur-Milchior/anki-fast-note-type-editor"), Addon("«Check database» Explain errors and what is done to fix it", 1135180054, gitHash = "371c360e5611ad3eec5dcef400d969e7b1572141", gitRepo = "https://github.com/Arthur-Milchior/anki-database-check-explained"), #mod unkwon because it's not directly used by the author anymore + Addon("Advanced note editor Multi-column Frozen fields", 2064123047, 1561905302, "82a27f2726598c25d06f3065d23eb988815efd25", "https://github.com/Arthur-Milchior/anki-Multi-column-edit-window"), + Addon("Multi-column note editor", 3491767031, 1560844854, "ad7a4014f184a1ec5d5d5c43a3fc4bab8bb8f6df", "https://github.com/hssm/anki-addons/tree/master/multi_column_editor"), } incorporatedAddonsDict = {**{addon.name: addon for addon in incorporatedAddonsSet if addon.name}, diff --git a/aqt/editor.py b/aqt/editor.py index e45f7433f7b..fc375d0125e 100644 --- a/aqt/editor.py +++ b/aqt/editor.py @@ -60,6 +60,7 @@ def __init__(self, mw, widget, parentWindow, addMode=False): self.widget = widget self.parentWindow = parentWindow self.note = None + self.model= None self.addMode = addMode self.currentField = None # current card, for card layout @@ -67,7 +68,7 @@ def __init__(self, mw, widget, parentWindow, addMode=False): self.setupOuter() self.setupWeb() self.setupShortcuts() - self.setupTags() + self.setupTagsLine() # Initial setup ############################################################ @@ -278,50 +279,58 @@ def onBridgeCmd(self, cmd): # shutdown return # focus lost or key/button pressed? - if cmd.startswith("blur") or cmd.startswith("key"): - (type, ord, nid, txt) = cmd.split(":", 3) - ord = int(ord) - try: - nid = int(nid) - except ValueError: - nid = 0 - if nid != self.note.id: - print("ignored late blur") - return - txt = urllib.parse.unquote(txt) - txt = unicodedata.normalize("NFC", txt) - txt = self.mungeHTML(txt) - # misbehaving apps may include a null byte in the text - txt = txt.replace("\x00", "") - # reverse the url quoting we added to get images to display - txt = self.mw.col.media.escapeImages(txt, unescape=True) - self.note.fields[ord] = txt - if not self.addMode: - self.note.flush() - self.mw.requireReset() - if type == "blur": - self.currentField = None - # run any filters - if runFilter( - "editFocusLost", False, self.note, ord): - # something updated the note; update it after a subsequent focus - # event has had time to fire - self.mw.progress.timer(100, self.loadNoteKeepingFocus, False) - else: - self.checkValid() - else: - runHook("editTimer", self.note) - self.checkValid() - # focused into field? - elif cmd.startswith("focus"): - (type, num) = cmd.split(":", 1) - self.currentField = int(num) - runHook("editFocusGained", self.note, self.currentField) - elif cmd in self._links: - self._links[cmd](self) + args = cmd.split(":") + cmd = args.pop(0) + if cmd in self._links: + self._links[cmd](self, *args) else: print("uncaught cmd", cmd) + def onBlur(self, *args): + self.onKeyOrBlur(*args) + self.currentField = None + # run any filters + if runFilter( + "editFocusLost", False, self.note, ord): + # something updated the note; update it after a subsequent focus + # event has had time to fire + self.mw.progress.timer(100, self.loadNoteKeepingFocus, False) + else: + self.checkValid() + + def onKey(self, *args): + self.onKeyOrBlur(*args) + runHook("editTimer", self.note) + self.checkValid() + + def onKeyOrBlur(self, ord, nid, *args): + ord = int(ord) + txt = ":".join(args) + try: + nid = int(nid) + except ValueError: + nid = 0 + if nid != self.note.id: + print("ignored late blur") + return + txt = urllib.parse.unquote(txt) + txt = unicodedata.normalize("NFC", txt) + txt = self.mungeHTML(txt) + # misbehaving apps may include a null byte in the text + txt = txt.replace("\x00", "") + # reverse the url quoting we added to get images to display + txt = self.mw.col.media.escapeImages(txt, unescape=True) + self.note.fields[ord] = txt + if not self.addMode: + self.note.flush() + self.mw.requireReset() + + def onFocus(self): + # focused into field? + (type, num) = cmd.split(":", 1) + self.currentField = int(num) + runHook("editFocusGained", self.note, self.currentField) + def mungeHTML(self, txt): if txt in ('
', '

'): return '' @@ -344,10 +353,12 @@ def setNote(self, note, hide=True, focusTo=None): focusTo -- in which field should the focus appear """ self.note = note - self.currentField = None if self.note: + self.model = note.model() + self.ccSpin.setValue(self.model.get("number of columns", 1)) self.loadNote(focusTo=focusTo) else: + self.model = None self.hideCompleters() if hide: self.widget.hide() @@ -362,9 +373,10 @@ def loadNote(self, focusTo=None): if not self.note: return - data = []# field name, field content modified so that it's image's url can be used locally. - for fld, val in list(self.note.items()): - data.append((fld, self.mw.col.media.escapeImages(val))) + # Triple, for each fields, with (field name, field + # content modified so that it's image's url can be + # used locally, and whether it is on its own line) + data = [(fld, self.mw.col.media.escapeImages(val), self.model["flds"][ord].get("Line alone", False)) for ord, (fld, val) in enumerate(self.note.items())] self.widget.show() self.updateTags() @@ -377,16 +389,18 @@ def oncallback(arg): self.web.setFocus() runHook("loadNote", self) - self.web.evalWithCallback("setFields(%s); setFonts(%s); focusField(%s); setNoteId(%s)" % ( + self.web.evalWithCallback("setFields(%s,%d); setFonts(%s); focusField(%s); setNoteId(%s)" % ( json.dumps(data), - json.dumps(self.fonts()), json.dumps(focusTo), - json.dumps(self.note.id)), + self.model.get("number of columns", 1), + json.dumps(self.fonts()), + json.dumps(focusTo), + json.dumps(self.note.id)), oncallback) def fonts(self): return [(runFilter("mungeEditingFontName", f['font']), f['size'], f['rtl']) - for f in self.note.model()['flds']] + for f in self.model['flds']] def saveNow(self, callback, keepFocus=False): "Save unsaved edits then call callback()." @@ -414,14 +428,14 @@ def showDupes(self): contents = stripHTMLMedia(self.note.fields[0]) browser = aqt.dialogs.open("Browser", self.mw) browser.form.searchEdit.lineEdit().setText( - '"dupe:%s,%s"' % (self.note.model()['id'], + '"dupe:%s,%s"' % (self.model['id'], contents)) browser.onSearchActivated() def fieldsAreBlank(self): if not self.note: return True - m = self.note.model() + m = self.model for c, f in enumerate(self.note.fields): if f and not m['flds'][c]['sticky']: return False @@ -457,25 +471,56 @@ def _onHtmlEdit(self, field): self.note.flush() self.loadNote(focusTo=field) + def setupTagsLine(self): + g = QGroupBox(self.widget) + g.setFlat(True) + tabLine = QHBoxLayout() + tabLine.setSpacing(12) + tabLine.setContentsMargins(6,6,6,6) + + self.setupTags(tabLine) + self.setupNumberColum(tabLine) + + self.outerLayout.addWidget(g) + g.setLayout(tabLine) + + + # Column handling + ###################################################################### + + def onColumnCountChanged(self, count): + "Save column count to settings and re-draw with new count." + self.model["number of columns"] = count + self.mw.col.models.save(self.model, recomputeReq=False) + self.loadNote() + + def setupNumberColum(self, tabLine): + label = QLabel("Columns:", self.widget) + tabLine.addWidget(label) + self.ccSpin = QSpinBox(self.widget) + tabLine.addWidget(self.ccSpin) + self.ccSpin.setMinimum(1) + self.ccSpin.valueChanged.connect(lambda value: self.onColumnCountChanged(value)) + + def onToggleLineAlone(self, fieldNumber): + fieldNumber = int(fieldNumber) + fieldObject = self.model['flds'][fieldNumber] + self.mw.col.models.save(self.model, recomputeReq=False) + fieldObject["Line alone"] = not fieldObject.get("Line alone", False) + self.loadNote() + # Tag handling ###################################################################### - def setupTags(self): + def setupTags(self, tabLine): import aqt.tagedit - g = QGroupBox(self.widget) - g.setFlat(True) - tb = QGridLayout() - tb.setSpacing(12) - tb.setContentsMargins(6,6,6,6) # tags - l = QLabel(_("Tags")) - tb.addWidget(l, 1, 0) + tagLabel = QLabel(_("Tags")) + tabLine.addWidget(tagLabel) self.tags = aqt.tagedit.TagEdit(self.widget) self.tags.lostFocus.connect(self.saveTags) self.tags.setToolTip(shortcut(_("Jump to tags with Ctrl+Shift+T"))) - tb.addWidget(self.tags, 1, 1) - g.setLayout(tb) - self.outerLayout.addWidget(g) + tabLine.addWidget(self.tags) def updateTags(self): if self.tags.col != self.mw.col: @@ -498,9 +543,8 @@ def saveAddModeVars(self): """During creation of new notes, save tags to the note's model""" if self.addMode: # save tags to model - m = self.note.model() - m['tags'] = self.note.tags - self.mw.col.models.save(m, recomputeReq=False) + self.model['tags'] = self.note.tags + self.mw.col.models.save(self.model, recomputeReq=False) def hideCompleters(self): "Remove tags's line" @@ -535,7 +579,7 @@ def onCloze(self): def _onCloze(self): # check that the model is set up for cloze deletion - if not re.search('{{(.*:)*cloze:',self.note.model()['tmpls'][0]['qfmt']): + if not re.search('{{(.*:)*cloze:',self.model['tmpls'][0]['qfmt']): if self.addMode: tooltip(_("Warning, cloze deletions will not work until " "you switch the type at the top to Cloze.")) @@ -874,6 +918,10 @@ def insertMathjaxChemistry(self): dupes=showDupes, paste=onPaste, cutOrCopy=onCutOrCopy, + key=onKey, + blur=onBlur, + toggleLineAlone=onToggleLineAlone, + #mceTrigger=onMceTrigger, ) # Pasting, drag & drop, and keyboard layouts diff --git a/difference.md b/difference.md index 6cd042d61ec..1abe77af8d4 100644 --- a/difference.md +++ b/difference.md @@ -2,6 +2,10 @@ This files list the difference between regular anki and this forked version. It also lists the different options in the Preferences's extra page. +## Multi column note editor (3491767031, 2064123047) +Allow to have editors with multiple columns. Allow to keep some fields +on their own line. + ## Explain errors You obtain more detailled error message if a sync fail, and if you try do do a «Check database». @@ -17,3 +21,14 @@ note, and let you correct the note instead to generate cards. ## Anki quicker (802285486) Those modification makes anki quicker. Technical details are on the add-on page. + +## Multi column note editor (3491767031, 2064123047) +Allow to have editors with multiple columns. Allow to keep some fields +on their own line. + +## Explain errors +You obtain more detailled error message if a sync fail, and if you try +do do a «Check database». + +It transform the very long method `fixIntegrity` into plenty of small +function. It would helps to do add-ons for this forked version of anki. diff --git a/web/editor.css b/web/editor.css index 6a51a19d62a..b1f7bea2798 100644 --- a/web/editor.css +++ b/web/editor.css @@ -21,7 +21,7 @@ } img { - max-width: 90vw; + max-width: 90%; } body { diff --git a/web/editor.js b/web/editor.js index 5ddfb1ee2af..6c8d6753234 100644 --- a/web/editor.js +++ b/web/editor.js @@ -263,6 +263,11 @@ function caretToEnd() { s.addRange(r); } +function changeSize(fieldNumber){ + saveNow(true); + pycmd("toggleLineAlone:"+fieldNumber); +} + function onBlur() { /*Tells python that it must save. Either by key if current field is still active. Otherwise by blur. If current field is not @@ -351,25 +356,56 @@ function onCutOrCopy() { return true; } -function setFields(fields) { +function createDiv(ord, fieldContent, nbCol){ + return "
{1}
".format(ord, fieldContent, nbCol); +} + +function createNameTd(ord, fieldName, nbColThisField, nbColTotal, sticky){ + txt = "{1}".format(nbColThisField, fieldName); + if (nbColTotal>1){ + txt+= "".format(ord); + } + return txt; +} + +function setFields(fields, nbCol) { /*Replace #fields by the HTML to show the list of fields to edit. Potentially change buttons - fields -- a list of fields, as (name of the field, current value) */ + fields -- a list of fields, as (name of the field, current value, whether it has its own line) + nbCol -- number of colum*/ var txt = ""; + var width = 100/nbCol; + var partialNames = ""; + var partialFields = ""; + var lengthLine = 0; for (var i = 0; i < fields.length; i++) { var fieldName = fields[i][0]; - var fieldValue = fields[i][1]; - if (!fieldValue) { - fieldValue = "
"; + var fieldContent = fields[i][1]; + var alone = fields[i][2]; + if (!fieldContent) { + fieldContent = "
"; } - txt += "{0}".format(fieldName); - txt += "
"+fieldContentHtml+""; + }else{ + lengthLine++; + partialNames += fieldNameHtml + partialFields += fieldContentHtml + } + //When a line is full, or last field, append it to txt. + if (lengthLine == nbCol || ( i == fields.length -1 && lengthLine>0)){ + txt+= ""+partialNames+""; + partialNames = ""; + txt+= ""+partialFields+""; + partialFields = ""; + lengthLine = 0; + } } $("#fields").html("" + txt + "
"); maybeDisableButtons(); From ffe775f72660e979f65e9f641429a0241d98d1c2 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Tue, 9 Jul 2019 01:28:45 +0200 Subject: [PATCH 65/68] Ensure image fit in fields --- web/editor.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/editor.css b/web/editor.css index b1f7bea2798..6a51a19d62a 100644 --- a/web/editor.css +++ b/web/editor.css @@ -21,7 +21,7 @@ } img { - max-width: 90%; + max-width: 90vw; } body { From f10ae7d47f4c90b10afd79bbd1f8c5eed5b0014d Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Mon, 17 Jun 2019 22:00:55 +0200 Subject: [PATCH 66/68] Quicker change model --- difference.md | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/difference.md b/difference.md index 1abe77af8d4..d2d5a656f53 100644 --- a/difference.md +++ b/difference.md @@ -2,9 +2,9 @@ This files list the difference between regular anki and this forked version. It also lists the different options in the Preferences's extra page. -## Multi column note editor (3491767031, 2064123047) -Allow to have editors with multiple columns. Allow to keep some fields -on their own line. +## Anki quicker (802285486) +Those modification makes anki quicker. Technical details are on the +add-on page. ## Explain errors You obtain more detailled error message if a sync fail, and if you try @@ -18,17 +18,6 @@ instead of deleting the note» chage the behavior of anki when he finds a note which has no more card. This allow to lose the content of the note, and let you correct the note instead to generate cards. -## Anki quicker (802285486) -Those modification makes anki quicker. Technical details are on the -add-on page. - ## Multi column note editor (3491767031, 2064123047) Allow to have editors with multiple columns. Allow to keep some fields on their own line. - -## Explain errors -You obtain more detailled error message if a sync fail, and if you try -do do a «Check database». - -It transform the very long method `fixIntegrity` into plenty of small -function. It would helps to do add-ons for this forked version of anki. From d64af1fc8b0b92a5bff0482c1072cce78266ceb5 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Mon, 17 Jun 2019 14:19:04 +0200 Subject: [PATCH 67/68] Frozen fields using a single button --- addons/516643804/LICENSE.txt | 680 ++++++++++++++++++++++++++++ addons/516643804/__init__.py | 14 + addons/516643804/_version.py | 36 ++ addons/516643804/config.json | 4 + addons/516643804/config.md | 6 + addons/516643804/config.py | 128 ++++++ addons/516643804/consts.py | 22 + addons/516643804/icons/frozen.png | Bin 0 -> 1111 bytes addons/516643804/icons/unfrozen.png | Bin 0 -> 969 bytes addons/516643804/main.py | 285 ++++++++++++ addons/516643804/manifest.json | 4 + addons/516643804/meta.json | 1 + anki/collection.py | 1 - aqt/addons.py | 3 + aqt/editor.py | 15 +- difference.md | 3 + web/editor.js | 21 +- web/imgs/frozen.png | Bin 0 -> 1111 bytes web/imgs/unfrozen.png | Bin 0 -> 969 bytes 19 files changed, 1213 insertions(+), 10 deletions(-) create mode 100644 addons/516643804/LICENSE.txt create mode 100644 addons/516643804/__init__.py create mode 100644 addons/516643804/_version.py create mode 100644 addons/516643804/config.json create mode 100644 addons/516643804/config.md create mode 100644 addons/516643804/config.py create mode 100644 addons/516643804/consts.py create mode 100644 addons/516643804/icons/frozen.png create mode 100644 addons/516643804/icons/unfrozen.png create mode 100644 addons/516643804/main.py create mode 100644 addons/516643804/manifest.json create mode 100644 addons/516643804/meta.json create mode 100644 web/imgs/frozen.png create mode 100644 web/imgs/unfrozen.png diff --git a/addons/516643804/LICENSE.txt b/addons/516643804/LICENSE.txt new file mode 100644 index 00000000000..baab77b6a78 --- /dev/null +++ b/addons/516643804/LICENSE.txt @@ -0,0 +1,680 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + +=============================================================================== + + ADDITIONAL TERMS APPLICABLE TO THIS PROGRAM + UNDER GNU AGPL VERSION 3 SECTION 7 + +The following additional terms ("Additional Terms") supplement and modify the +GNU Affero General Public License, Version 3 ("AGPL") applicable to the present +Program. + +In addition to the terms and conditions of the AGPL, the present Program is +subject to the further restrictions below: + +1. Trademark and Publicity Rights. + + Except as expressly provided herein, no trademark or publicity rights are + granted. This license does NOT give you any right, title or interest in the + "Glutanimate" name or logo. + +2. Origin of the Program. + + The origin of the Program must not be misrepresented; you must not claim + that you wrote the original Program. Altered source versions must be plainly + marked as such, and must not be misrepresented as being the original + Program. + +3. Legal Notices and Author Attributions. + + You must reproduce faithfully all trademark, copyright and other proprietary + and legal notices on any copies of the Program or any other required author + attributions. Legal notices or author attributions displayed as part of the + user interface must be preserved as such. + +4. Use of Names of Licensors or Authors for Publicity Purposes. + + Outside of the aforementioned legal notices and author attributions, neither + the name of the copyright holder or its affiliates, any other party who + modifies and/or conveys the Program, nor the names of the Program's + sponsors/supporters/patrons may be used to endorse or promote products + derived from this software without specific prior written permission. + +5. Indemnification. + + IF YOU CONVEY A COVERED WORK AND AGREE WITH ANY RECIPIENT OF THAT COVERED + WORK THAT YOU WILL ASSUME ANY LIABILITY FOR THAT COVERED WORK, YOU HEREBY + AGREE TO INDEMNIFY, DEFEND AND HOLD HARMLESS THE OTHER LICENSORS AND AUTHORS + OF THAT COVERED WORK FOR ANY DAMAGES, DEMANDS, CLAIMS, LOSSES, CAUSES OF + ACTION, LAWSUITS, JUDGMENTS EXPENSES (INCLUDING WITHOUT LIMITATION + REASONABLE ATTORNEYS' FEES AND EXPENSES) OR ANY OTHER LIABLITY ARISING FROM, + RELATED TO OR IN CONNECTION WITH YOUR ASSUMPTIONS OF LIABILITY. + +6. Preservation of Licensing Terms. + + Any covered work conveyed by you must include this license text in its + entirety. + +------------------------------------------------------------------------------- + +If you have any questions regarding this license, about any other legal +details, or want to report an infringement of the aforementioned licensing +terms, please feel free to contact me at: diff --git a/addons/516643804/__init__.py b/addons/516643804/__init__.py new file mode 100644 index 00000000000..651099849aa --- /dev/null +++ b/addons/516643804/__init__.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- + +""" +Anki Add-on: Frozen Fields + +Module-level entry point for the add-on into Anki 2.0.x/2.1.x + +Please do not edit this file if you are not sure what you are doing. + +Copyright: (c) 2018 Glutanimate +License: GNU AGPLv3 +""" + +from . import main diff --git a/addons/516643804/_version.py b/addons/516643804/_version.py new file mode 100644 index 00000000000..6246d8ee8cc --- /dev/null +++ b/addons/516643804/_version.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- + +# Frozen Fields Add-on for Anki +# +# Copyright (C) 2019 Aristotelis P. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version, with the additions +# listed at the end of the license file that accompanied this program. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# NOTE: This program is subject to certain additional terms pursuant to +# Section 7 of the GNU Affero General Public License. You should have +# received a copy of these additional terms immediately following the +# terms and conditions of the GNU Affero General Public License that +# accompanied this program. +# +# If not, please request a copy through one of the means of contact +# listed here: . +# +# Any modifications to this file must keep this entire header intact. + +""" +Version information +""" + +__version__ = "2.0.2" diff --git a/addons/516643804/config.json b/addons/516643804/config.json new file mode 100644 index 00000000000..0c348fd1beb --- /dev/null +++ b/addons/516643804/config.json @@ -0,0 +1,4 @@ +{ + "hotkeyOne": "F9", + "hotkeyAll": "Shift+F9" +} \ No newline at end of file diff --git a/addons/516643804/config.md b/addons/516643804/config.md new file mode 100644 index 00000000000..359074448c0 --- /dev/null +++ b/addons/516643804/config.md @@ -0,0 +1,6 @@ +### Frozen Fields: Advanced Configuration + +These advanced settings do not sync and require a restart to apply. + +- `hotkeyOne` [string]: Hotkey that toggles status for current field Default: `F9` +- `hotkeyAll` [string]: Hotkey that toggles status for all fields. Default: `Shift+F9` \ No newline at end of file diff --git a/addons/516643804/config.py b/addons/516643804/config.py new file mode 100644 index 00000000000..48a5df9b75d --- /dev/null +++ b/addons/516643804/config.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- + +""" +This file is part of the Frozen Fields add-on for Anki. + +Configuration shim between Anki 2.0 and Anki 2.1 + +Copyright: (c) 2018 Glutanimate +License: GNU AGPLv3 +""" + +import os +import io + +from aqt import mw +from anki.utils import json + +from .consts import * + +defaults_path = os.path.join(addon_path, "config.json") +meta_path = os.path.join(addon_path, "meta.json") + +if anki21: + def getConfig(): + return mw.addonManager.getConfig(__name__) + + def writeConfig(config): + mw.addonManager.writeConfig(__name__, config) + +else: + def _addonMeta(): + """Get meta dictionary + + Reads in meta.json in add-on folder and returns + resulting dictionary of user-defined metadata values. + + Note: + Anki 2.1 stores both add-on meta data and customized + settings in meta.json. In this module we are only dealing + with the settings part. + + Returns: + dict: config dictionary + + """ + + try: + meta = json.load(io.open(meta_path, encoding="utf-8")) + except (IOError, OSError): + meta = None + except json.decoder.JSONDecodeError as e: + print("Could not read meta.json: " + str(e)) + meta = None + + if not meta: + meta = {"config": _addonConfigDefaults()} + _writeAddonMeta(meta) + + return meta + + def _writeAddonMeta(meta): + """Write meta dictionary + + Writes meta dictionary to meta.json in add-on folder. + + Args: + meta (dict): meta dictionary + + """ + + with io.open(meta_path, 'w', encoding="utf-8") as f: + f.write(unicode(json.dumps(meta, indent=4, + sort_keys=True, + ensure_ascii=False))) + + def _addonConfigDefaults(): + """Get default config dictionary + + Reads in config.json in add-on folder and returns + resulting dictionary of default config values. + + Returns: + dict: config dictionary + + Raises: + Exception: If config.json cannot be parsed correctly. + (The assumption being that we would end up in an + inconsistent state if we were to return an empty + config dictionary. This should never happen.) + + """ + + try: + return json.load(io.open(defaults_path, encoding="utf-8")) + except (IOError, OSError, json.decoder.JSONDecodeError) as e: + print("Could not read config.json: " + str(e)) + raise Exception("Config file could not be read: " + str(e)) + + def getConfig(): + """Get user config dictionary + + Merges user's keys into default config dictionary + and returns the result. + + Returns: + dict: config dictionary + + """ + + config = _addonConfigDefaults() + meta = _addonMeta() + userConf = meta.get("config", {}) + config.update(userConf) + return config + + def writeConfig(config): + """Write user config dictionary + + Saves user's config dictionary via meta.json. + + Args: + config (dict): user config dictionary + + """ + + _writeAddonMeta({"config": config}) + +local_conf = getConfig() diff --git a/addons/516643804/consts.py b/addons/516643804/consts.py new file mode 100644 index 00000000000..2d245f50ef2 --- /dev/null +++ b/addons/516643804/consts.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- + +""" +This file is part of the Frozen Fields add-on for Anki. + +Global variables + +Copyright: (c) 2018 Glutanimate +License: GNU AGPLv3 +""" + +import sys +import os +from anki import version + +anki21 = version.startswith("2.1.") +sys_encoding = sys.getfilesystemencoding() + +if anki21: + addon_path = os.path.dirname(__file__) +else: + addon_path = os.path.dirname(__file__).decode(sys_encoding) diff --git a/addons/516643804/icons/frozen.png b/addons/516643804/icons/frozen.png new file mode 100644 index 0000000000000000000000000000000000000000..83fc7d28773064e8c0cb81426887a5f10877eda4 GIT binary patch literal 1111 zcmV-d1gQIoP)^s+ zUBgA33>cTe1PUr)pkp_YA1?c&(abTRcfNkCh5GgJJ$w?Qz;sLf@gyhbeRH1kp7WgZ zN&rayPyF|bD%>8LZA+3W?E56DUqXUT5TSIWZLbi76hi>(R4!dm3D&8dMX`GC&j;!O zLV{s-JWAs#2nvDZz;6#E7`@iKWVr2$z=GW^*<)R?CeOb0sX}+L zG}jSM{+D#_uhngWaQI7rpl%lg^3mB!)#O960!MzN(z=QIBuT$`=aK~XXH3eX3;3~3 zrCwXA$v>k!4!o}C+UGAxaNFie?qux%KzNhctSizbd$93SzK^n}j0_f_;k1}>-Dv@>XMv7$BHEc< z&wNea0mp`b>V81mDZb)0-Hc_~Ca>2xOH$3VT)dGVjWtYnSN8ut*UW;nv4AJf1E(&E z=)543w44D<-w1yBQI93bJoDq8dcRLr@b?*6EUOQvR!Y*Z-9vVCsY9;r^j&RdS$DC( zJ)=PH4JUJ80In~9Kv*CK96@TA(Ez2oStTV6dg7H6Ec7`_txROi+aF=HwXp@Wr+ zdP3zh4+w#6)!b0B`TFJEb^lVL4JCXJDS;GG-Xm(hahNrEP(aIw*b4$Fsh0Ujw)KNR zWiL?NArkhBT z1o>Zsc1N7iGpuQ4Gw=nlw+7g009NO^@)i+K_9^5AGwYM|I=p^8g|H<@Mx|x>z|)W dPzY`ne*kBNVB*gAG&cYM002ovPDHLkV1hcr9|r&c literal 0 HcmV?d00001 diff --git a/addons/516643804/icons/unfrozen.png b/addons/516643804/icons/unfrozen.png new file mode 100644 index 0000000000000000000000000000000000000000..53aa2c29b1cc1429a7ebcf3023bbf34b3936dbe2 GIT binary patch literal 969 zcmV;)12+7LP)o~p|pW~?GIOA&^ z6&Hn@+C)MbWC%edSb(f zY)o))@I}7^#Kpyhm6w;hD=RA{IXPMHSAdk1lp>u@_pzm=MQ9Zj729cPY59Hu2n!3l ziiA5oJw4Lf+p8G;s;a7;prD{@Cjtlz47>!pl8}(_EF&XBo0yn*oAJE7yqUhfKKIDT zh|r8iqnk0FB_$1JVJ;kTNanx9==U82=$hm=by z)tQo#lD9K6GeV=!7|(FW$QM;tSML@T6@5Yu7sT-32tZ6sOfsG=&CSiy+1V-m{ryr} zTIxVm7kG{Y2D{xZH2NN8fj;ZNz<_jjcPstDnLuet-T(q}b93LfwY7=CU=WkZB$Jbq z(%9Izk(HGdQCnNZ+Y!Oet!NG zt7kXr=wOVCqX-SrsmldPWn8V=^o6RO>vss3QhLl$Tzbw%RZz#s&umx6y}CkDM{#_dLx%79b=fB!E&qY+^8zuv%8_a=8?v zPk`a!;V+;hnn^hm0J-ZE028amQ-5F`Y=RwX$Lwv~EjJPY=b}3X_8>Wrei1whDIE4(8zT5rFvk_-t%TCKS&s z==FN1r{PeFfs&-rKkU&t4kOlxbaZqGC=za4Y+jAG7u-h=Y?+yv8@RMbsxbOfo{2?e zCe8*2i#}sKLz73C9232Z;ai2&rvPxr=QhP_o?{(rJv#-9&59v9;QBaR3kQrH-o1&W z8Z5@Ar&`yPAE^3D+E^)#uLCvTPrR;u{BW{C*N6K0dZkAMMC^@Uf0 + (c) 2015-2018 Glutanimate + +License: GNU AGPLv3 +""" + + +import os + +from aqt.qt import * + +from aqt.editor import Editor +from aqt.addcards import AddCards + +from anki.hooks import wrap, addHook, runHook +from anki.utils import json + +from .consts import * +from .config import local_conf + +icon_path = os.path.join(addon_path, "icons") + +icon_path_frozen = os.path.join(icon_path, "frozen.png") +icon_path_unfrozen = os.path.join(icon_path, "unfrozen.png") + +icon_frozen = QUrl.fromLocalFile(icon_path_frozen).toString() +icon_unfrozen = QUrl.fromLocalFile(icon_path_unfrozen).toString() + +hotkey_toggle_field = local_conf["hotkeyOne"] +hotkey_toggle_all = local_conf["hotkeyAll"] + + +js_code_20 = """ +function onFrozen(elem) { + currentField = elem; + py.run("frozen:" + currentField.id.substring(1)); +} + +function setFrozenFields(fields, frozen, focusTo) { + var txt = ""; + for (var i=0; i"+txt+""); + if (!focusTo) { + focusTo = 0; + } + if (focusTo >= 0) { + $("#f"+focusTo).focus(); + } +}; +""" % (hotkey_toggle_field, icon_frozen, hotkey_toggle_field, icon_unfrozen) + + +js_code_21 = """ +function onFrozen(elem) { + currentField = elem; + pycmd("frozen:" + currentField.id.substring(1)); +} + +function setFrozenFields(fields, frozen) { + var txt = ""; + for (var i=0; i" + txt + ""); + maybeDisableButtons(); +} + +""" + + +def loadNote20(self): + """Modified loadNote(), adds buttons to Editor""" + if not self.note: + return + if self.stealFocus: + field = self.currentField + else: + field = -1 + if not self._loaded: + # will be loaded when page is ready + return + data = [] + for fld, val in self.note.items(): + data.append((fld, self.mw.col.media.escapeImages(val))) + ###### ↓modified ######### + if isinstance(self.parentWindow, AddCards): # only modify AddCards Editor + flds = self.note.model()["flds"] + sticky = [fld["sticky"] for fld in flds] + self.web.eval(js_code_20) + self.web.eval("setFrozenFields(%s, %s, %d);" % ( + json.dumps(data), json.dumps(sticky), field)) + else: + self.web.eval("setFields(%s, %d);" % ( + json.dumps(data), field)) + ########################### + self.web.eval("setFonts(%s);" % ( + json.dumps(self.fonts()))) + self.checkValid() + self.widget.show() + if self.stealFocus: + self.web.setFocus() + # self.stealFocus = False + + +def loadNote21(self, focusTo=None): + if not self.note: + return + + data = [] + for fld, val in list(self.note.items()): + data.append((fld, self.mw.col.media.escapeImages(val))) + self.widget.show() + self.updateTags() + + def oncallback(arg): + if not self.note: + return + self.setupForegroundButton() + self.checkValid() + if focusTo is not None: + self.web.setFocus() + runHook("loadNote", self) + + # only modify AddCards Editor + if not isinstance(self.parentWindow, AddCards): + self.web.evalWithCallback("setFields(%s); setFonts(%s); focusField(%s); setNoteId(%s)" % ( + json.dumps(data), + json.dumps(self.fonts()), json.dumps(focusTo), + json.dumps(self.note.id)), + oncallback) + else: + iconstr_frozen = self.resourceToData(icon_path_frozen) + iconstr_unfrozen = self.resourceToData(icon_path_unfrozen) + + flds = self.note.model()["flds"] + sticky = [fld["sticky"] for fld in flds] + + eval_definitions = js_code_21 % (hotkey_toggle_field, iconstr_frozen, + hotkey_toggle_field, iconstr_unfrozen) + + eval_calls = "setFrozenFields(%s, %s); setFonts(%s); focusField(%s); setNoteId(%s)" % ( + json.dumps(data), json.dumps(sticky), + json.dumps(self.fonts()), + json.dumps(focusTo), + json.dumps(self.note.id)) + + self.web.eval(eval_definitions) + self.web.evalWithCallback(eval_calls, oncallback) + + +def onBridge(self, str, _old): + """Extends the js<->py bridge with our pycmd handler""" + + if not str.startswith("frozen"): + if anki21 and str.startswith("blur"): + self.lastField = self.currentField # save old focus + return _old(self, str) + if not self.note or not runHook: + # shutdown + return + + (cmd, txt) = str.split(":", 1) + cur = int(txt) + flds = self.note.model()['flds'] + flds[cur]['sticky'] = not flds[cur]['sticky'] + + if anki21: + # load and restore old focus + self.loadNote(focusTo=getattr(self, "lastField", None)) + else: + self.loadNote() + + +def frozenToggle(self, batch=False): + """Toggle state of current field""" + + flds = self.note.model()['flds'] + cur = self.currentField + if cur is None: + cur = 0 + is_sticky = flds[cur]["sticky"] + if not batch: + flds[cur]["sticky"] = not is_sticky + else: + for n in range(len(self.note.fields)): + try: + flds[n]['sticky'] = not is_sticky + except IndexError: + break + + if anki21: + self.loadNoteKeepingFocus() + else: + self.web.eval("saveField('key');") + self.loadNote() + +def onFrozenToggle21(self, batch=False): + self.web.evalWithCallback("saveField('key');", lambda _: self.frozenToggle(batch=batch)) + + +def onSetupButtons20(self): + """Set up hotkeys""" + if not isinstance(self.parentWindow, AddCards): # only modify AddCards Editor + return + + QShortcut(QKeySequence(hotkey_toggle_field), self.parentWindow, + activated=self.frozenToggle) + QShortcut(QKeySequence(hotkey_toggle_all), self.parentWindow, + activated=lambda: self.frozenToggle(batch=True)) + + +def onSetupShortcuts21(cuts, self): + cuts += [(hotkey_toggle_field, self.onFrozenToggle), + (hotkey_toggle_all, lambda: self.onFrozenToggle(batch=True), True)] + # third value: enable shortcut even when no field selected + +# Add-on hooks, etc. + + +if anki21: + addHook("setupEditorShortcuts", onSetupShortcuts21) + Editor.onBridgeCmd = wrap(Editor.onBridgeCmd, onBridge, "around") + Editor.loadNote = loadNote21 + Editor.onFrozenToggle = onFrozenToggle21 +else: + addHook("setupEditorButtons", onSetupButtons20) + Editor.bridge = wrap(Editor.bridge, onBridge, 'around') + Editor.loadNote = loadNote20 + +Editor.frozenToggle = frozenToggle diff --git a/addons/516643804/manifest.json b/addons/516643804/manifest.json new file mode 100644 index 00000000000..67228b1acfa --- /dev/null +++ b/addons/516643804/manifest.json @@ -0,0 +1,4 @@ +{ + "name": "Frozen Fields", + "package": "516643804" +} \ No newline at end of file diff --git a/addons/516643804/meta.json b/addons/516643804/meta.json new file mode 100644 index 00000000000..a78cb793d04 --- /dev/null +++ b/addons/516643804/meta.json @@ -0,0 +1 @@ +{"name": "Frozen Fields", "mod": 1561600792} \ No newline at end of file diff --git a/anki/collection.py b/anki/collection.py index 00586447735..d2d5a2bb9c6 100644 --- a/anki/collection.py +++ b/anki/collection.py @@ -1168,7 +1168,6 @@ def atMost1000000Due(self): if curs.rowcount: self.problems.append("Found %d new cards with a due number >= 1,000,000 - consider repositioning them in the Browse screen." % curs.rowcount) - def setNextPos(self): # new card position self.conf['nextPos'] = self.db.scalar( diff --git a/aqt/addons.py b/aqt/addons.py index c08b4c1c144..32fb4d8f1ee 100644 --- a/aqt/addons.py +++ b/aqt/addons.py @@ -902,9 +902,12 @@ def __hash__(self): """ Set of characteristic of Add-ons incorporated here""" incorporatedAddonsSet = { Addon("Adding note and changing note type become quicker", 802285486, gitHash = "f1b2df03f4040e7820454052a2088a7672d819b2", gitRepo = "https://github.com/Arthur-Milchior/anki-fast-note-type-editor"), + Addon("Advanced note editor Multi-column Frozen fields", 2064123047, 1561905302, "82a27f2726598c25d06f3065d23eb988815efd25", "https://github.com/Arthur-Milchior/anki-Multi-column-edit-window"), Addon("«Check database» Explain errors and what is done to fix it", 1135180054, gitHash = "371c360e5611ad3eec5dcef400d969e7b1572141", gitRepo = "https://github.com/Arthur-Milchior/anki-database-check-explained"), #mod unkwon because it's not directly used by the author anymore Addon("Advanced note editor Multi-column Frozen fields", 2064123047, 1561905302, "82a27f2726598c25d06f3065d23eb988815efd25", "https://github.com/Arthur-Milchior/anki-Multi-column-edit-window"), + Addon("Frozen Fields", 516643804, 1561600792, "191bbb759b3a9554e88fa36ba20b83fe68187f2d", "https://github.com/glutanimate/frozen-fields"), Addon("Multi-column note editor", 3491767031, 1560844854, "ad7a4014f184a1ec5d5d5c43a3fc4bab8bb8f6df", "https://github.com/hssm/anki-addons/tree/master/multi_column_editor"), + Addon("Multi-column note editor debugged", 2064123047, 1550534156, "70f92cd5f62bd4feda5422701bd01acb41ed48ce", "https://github.com/Arthur-Milchior/anki-Multi-column-edit-window"), } incorporatedAddonsDict = {**{addon.name: addon for addon in incorporatedAddonsSet if addon.name}, diff --git a/aqt/editor.py b/aqt/editor.py index fc375d0125e..02792ba7f2f 100644 --- a/aqt/editor.py +++ b/aqt/editor.py @@ -376,7 +376,11 @@ def loadNote(self, focusTo=None): # Triple, for each fields, with (field name, field # content modified so that it's image's url can be # used locally, and whether it is on its own line) - data = [(fld, self.mw.col.media.escapeImages(val), self.model["flds"][ord].get("Line alone", False)) for ord, (fld, val) in enumerate(self.note.items())] + data = [(fld, + self.mw.col.media.escapeImages(val), + self.model["flds"][ord].get("Line alone", False), + self.model["flds"][ord].get("sticky", False) + ) for ord, (fld, val) in enumerate(self.note.items())] self.widget.show() self.updateTags() @@ -509,6 +513,13 @@ def onToggleLineAlone(self, fieldNumber): fieldObject["Line alone"] = not fieldObject.get("Line alone", False) self.loadNote() + def onFroze(self, fieldNumber): + fieldNumber = int(fieldNumber) + fieldObject = self.model['flds'][fieldNumber] + fieldObject["sticky"] = not fieldObject.get("sticky", False) + self.mw.col.models.save(self.model) + self.loadNote() + # Tag handling ###################################################################### @@ -921,7 +932,7 @@ def insertMathjaxChemistry(self): key=onKey, blur=onBlur, toggleLineAlone=onToggleLineAlone, - #mceTrigger=onMceTrigger, + toggleFroze=onFroze, ) # Pasting, drag & drop, and keyboard layouts diff --git a/difference.md b/difference.md index d2d5a656f53..9c4244874f7 100644 --- a/difference.md +++ b/difference.md @@ -18,6 +18,9 @@ instead of deleting the note» chage the behavior of anki when he finds a note which has no more card. This allow to lose the content of the note, and let you correct the note instead to generate cards. +## Frozen Fields (516643804) +Add a small icon near the name of the field to make it sticky or not. + ## Multi column note editor (3491767031, 2064123047) Allow to have editors with multiple columns. Allow to keep some fields on their own line. diff --git a/web/editor.js b/web/editor.js index 6c8d6753234..dac4c5dfce9 100644 --- a/web/editor.js +++ b/web/editor.js @@ -60,7 +60,6 @@ function onKey() { If no other action is done in .6 seconds, tell Python what change did occur */ - // esc clears focus, allowing dialog to close if (window.event.which === 27) { currentField.blur(); @@ -187,7 +186,8 @@ function onFocus(elem) { return; } currentField = elem; - pycmd("focus:" + currentFieldOrdinal()); + cmd = "focus:" + currentFieldOrdinal(); + pycmd(cmd); enableButtons(); // don't adjust cursor on mouse clicks if (mouseDown) { @@ -268,6 +268,11 @@ function changeSize(fieldNumber){ pycmd("toggleLineAlone:"+fieldNumber); } +function toggleFroze(fieldNumber){ + saveNow(true); + pycmd("toggleFroze:"+fieldNumber); +} + function onBlur() { /*Tells python that it must save. Either by key if current field is still active. Otherwise by blur. If current field is not @@ -297,8 +302,9 @@ function saveField(type) { // no field has been focused yet return; } + cmd = type + ":" + currentFieldOrdinal() + ":" + currentNoteId + ":" + currentField.innerHTML; // type is either 'blur' or 'key' - pycmd(type + ":" + currentFieldOrdinal() + ":" + currentNoteId + ":" + currentField.innerHTML); + pycmd(cmd); } function currentFieldOrdinal() { @@ -361,10 +367,13 @@ function createDiv(ord, fieldContent, nbCol){ } function createNameTd(ord, fieldName, nbColThisField, nbColTotal, sticky){ + img = (sticky?"":"un")+"frozen.png"; + title =(sticky?"Unf":"F")+"reeze field "+fieldName; txt = "{1}".format(nbColThisField, fieldName); if (nbColTotal>1){ txt+= "".format(ord); } + txt+="".format(title, img, ord); return txt; } @@ -383,13 +392,13 @@ function setFields(fields, nbCol) { var fieldName = fields[i][0]; var fieldContent = fields[i][1]; var alone = fields[i][2]; + var sticky = fields[i][3]; if (!fieldContent) { fieldContent = "
"; } - //console.log("fieldName: "+fieldName+", fieldContent: "+fieldContent+", alone: "+alone); nbColThisField = (alone)?nbCol:1; fieldContentHtml = createDiv(i, fieldContent, nbColThisField); - fieldNameHtml = createNameTd(i, fieldName, nbColThisField, nbCol) + fieldNameHtml = createNameTd(i, fieldName, nbColThisField, nbCol, sticky) if (alone){ nameTd = fieldNameHtml txt += ""+fieldNameHtml+""+fieldContentHtml+""; @@ -472,8 +481,6 @@ var filterHTML = function (html, internal, extendedMode) { outHtml = outHtml.replace(/[\n\t ]+/g, " "); } outHtml = outHtml.trim(); - //console.log(`input html: ${html}`); - //console.log(`outpt html: ${outHtml}`); return outHtml; }; diff --git a/web/imgs/frozen.png b/web/imgs/frozen.png new file mode 100644 index 0000000000000000000000000000000000000000..83fc7d28773064e8c0cb81426887a5f10877eda4 GIT binary patch literal 1111 zcmV-d1gQIoP)^s+ zUBgA33>cTe1PUr)pkp_YA1?c&(abTRcfNkCh5GgJJ$w?Qz;sLf@gyhbeRH1kp7WgZ zN&rayPyF|bD%>8LZA+3W?E56DUqXUT5TSIWZLbi76hi>(R4!dm3D&8dMX`GC&j;!O zLV{s-JWAs#2nvDZz;6#E7`@iKWVr2$z=GW^*<)R?CeOb0sX}+L zG}jSM{+D#_uhngWaQI7rpl%lg^3mB!)#O960!MzN(z=QIBuT$`=aK~XXH3eX3;3~3 zrCwXA$v>k!4!o}C+UGAxaNFie?qux%KzNhctSizbd$93SzK^n}j0_f_;k1}>-Dv@>XMv7$BHEc< z&wNea0mp`b>V81mDZb)0-Hc_~Ca>2xOH$3VT)dGVjWtYnSN8ut*UW;nv4AJf1E(&E z=)543w44D<-w1yBQI93bJoDq8dcRLr@b?*6EUOQvR!Y*Z-9vVCsY9;r^j&RdS$DC( zJ)=PH4JUJ80In~9Kv*CK96@TA(Ez2oStTV6dg7H6Ec7`_txROi+aF=HwXp@Wr+ zdP3zh4+w#6)!b0B`TFJEb^lVL4JCXJDS;GG-Xm(hahNrEP(aIw*b4$Fsh0Ujw)KNR zWiL?NArkhBT z1o>Zsc1N7iGpuQ4Gw=nlw+7g009NO^@)i+K_9^5AGwYM|I=p^8g|H<@Mx|x>z|)W dPzY`ne*kBNVB*gAG&cYM002ovPDHLkV1hcr9|r&c literal 0 HcmV?d00001 diff --git a/web/imgs/unfrozen.png b/web/imgs/unfrozen.png new file mode 100644 index 0000000000000000000000000000000000000000..53aa2c29b1cc1429a7ebcf3023bbf34b3936dbe2 GIT binary patch literal 969 zcmV;)12+7LP)o~p|pW~?GIOA&^ z6&Hn@+C)MbWC%edSb(f zY)o))@I}7^#Kpyhm6w;hD=RA{IXPMHSAdk1lp>u@_pzm=MQ9Zj729cPY59Hu2n!3l ziiA5oJw4Lf+p8G;s;a7;prD{@Cjtlz47>!pl8}(_EF&XBo0yn*oAJE7yqUhfKKIDT zh|r8iqnk0FB_$1JVJ;kTNanx9==U82=$hm=by z)tQo#lD9K6GeV=!7|(FW$QM;tSML@T6@5Yu7sT-32tZ6sOfsG=&CSiy+1V-m{ryr} zTIxVm7kG{Y2D{xZH2NN8fj;ZNz<_jjcPstDnLuet-T(q}b93LfwY7=CU=WkZB$Jbq z(%9Izk(HGdQCnNZ+Y!Oet!NG zt7kXr=wOVCqX-SrsmldPWn8V=^o6RO>vss3QhLl$Tzbw%RZz#s&umx6y}CkDM{#_dLx%79b=fB!E&qY+^8zuv%8_a=8?v zPk`a!;V+;hnn^hm0J-ZE028amQ-5F`Y=RwX$Lwv~EjJPY=b}3X_8>Wrei1whDIE4(8zT5rFvk_-t%TCKS&s z==FN1r{PeFfs&-rKkU&t4kOlxbaZqGC=za4Y+jAG7u-h=Y?+yv8@RMbsxbOfo{2?e zCe8*2i#}sKLz73C9232Z;ai2&rvPxr=QhP_o?{(rJv#-9&59v9;QBaR3kQrH-o1&W z8Z5@Ar&`yPAE^3D+E^)#uLCvTPrR;u{BW{C*N6K0dZkAMMC^@Uf0 Date: Sun, 14 Jul 2019 08:24:58 -0700 Subject: [PATCH 68/68] Add license scan report and status Signed-off-by: fossabot --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 910436e9e3a..004211f0ae3 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ # Forked Anki, Alpha version +[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FArthur-Milchior%2Fanki.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2FArthur-Milchior%2Fanki?ref=badge_shield) + ------------------------------------- This is the development branch of a forked version of Anki. For the official, please see [https://apps.ankiweb.net](https://apps.ankiweb.net). @@ -31,3 +33,7 @@ For a detailled explanation, see [How to add an add-on in the code.md](How to ad To run from source, please see README.development. If you are interested in contributing changes to this Fork, just contact arthur@milchior.fr + + +## License +[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FArthur-Milchior%2Fanki.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2FArthur-Milchior%2Fanki?ref=badge_large) \ No newline at end of file