From 8812472b5381bae00375be34a00ccb3e2da63c26 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 14 Nov 2013 11:14:21 +0900 Subject: [PATCH 01/15] catch "no such file or directory" error when connecting with no net soren reports it happening on his computer; can't repro it here also make sure exception is always converted to string in reliable way --- aqt/sync.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/aqt/sync.py b/aqt/sync.py index 8fc9bb1f1..c3d09a6bf 100644 --- a/aqt/sync.py +++ b/aqt/sync.py @@ -337,12 +337,9 @@ class SyncThread(QThread): ret = self.client.sync() except Exception, e: log = traceback.format_exc() - try: - err = unicode(e[0], "utf8", "ignore") - except: - # number, exception with no args, etc - err = "" - if "Unable to find the server" in err: + err = repr(str(e)) + if ("Unable to find the server" in err or + "Errno 2" in err): self.fireEvent("offline") else: if not err: From 0389eebc43209ca7f2b45b4f086f7301beb8ba31 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 14 Nov 2013 12:12:37 +0900 Subject: [PATCH 02/15] ask people to check media when media sanity occurs --- aqt/sync.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aqt/sync.py b/aqt/sync.py index c3d09a6bf..7d85cc7bc 100644 --- a/aqt/sync.py +++ b/aqt/sync.py @@ -126,8 +126,8 @@ Please visit AnkiWeb, upgrade your deck, then try again.""")) self._checkFailed() elif evt == "mediaSanity": showWarning(_("""\ -A problem occurred while syncing media. Please sync again and Anki will \ -correct the issue.""")) +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": From 4bf63b6ad037380f971419a881e569467a58ee01 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 14 Nov 2013 14:41:31 +0900 Subject: [PATCH 03/15] bump version --- anki/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/anki/__init__.py b/anki/__init__.py index 8c7cb6488..c45d39df6 100644 --- a/anki/__init__.py +++ b/anki/__init__.py @@ -30,6 +30,6 @@ if arch[1] == "ELF": sys.path.insert(0, os.path.join(ext, "py2.%d-%s" % ( sys.version_info[1], arch[0][0:2]))) -version="2.0.17" # build scripts grep this line, so preserve formatting +version="2.0.18" # build scripts grep this line, so preserve formatting from anki.storage import Collection __all__ = ["Collection"] From ae8074ec01a8f09a13e7e1e020b848ed19b26bfe Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sun, 17 Nov 2013 16:03:58 +0900 Subject: [PATCH 04/15] make sure we reset odue when rescheduling as new if we fail to do this for a relearning card, it sticks around until it causes problems later --- anki/cards.py | 15 +++------------ anki/collection.py | 10 ++++++++++ anki/sched.py | 2 +- aqt/main.py | 6 ++++++ 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/anki/cards.py b/anki/cards.py index ee9f7b8c1..5d8e2fb18 100644 --- a/anki/cards.py +++ b/anki/cards.py @@ -4,19 +4,10 @@ import pprint import time +from anki.hooks import runHook from anki.utils import intTime, timestampID, joinFields from anki.consts import * -# temporary -_warned = False -def warn(): - global _warned - if _warned: - return - import sys - sys.stderr.write("Ignore the above, please download the fix assertion addon.") - _warned = True - # Cards ########################################################################## @@ -83,7 +74,7 @@ class Card(object): self.usn = self.col.usn() # bug check if self.queue == 2 and self.odue and not self.col.decks.isDyn(self.did): - warn() + runHook("odueInvalid") assert self.due < 4294967296 self.col.db.execute( """ @@ -114,7 +105,7 @@ insert or replace into cards values self.usn = self.col.usn() # bug checks if self.queue == 2 and self.odue and not self.col.decks.isDyn(self.did): - warn() + runHook("odueInvalid") assert self.due < 4294967296 self.col.db.execute( """update cards set diff --git a/anki/collection.py b/anki/collection.py index 706c0ff78..fba9d5899 100644 --- a/anki/collection.py +++ b/anki/collection.py @@ -741,6 +741,16 @@ select id from cards where nid not in (select id from notes)""") 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)) # tags self.tags.registerNotes() # field cache diff --git a/anki/sched.py b/anki/sched.py index 2f52e557e..189dd1582 100644 --- a/anki/sched.py +++ b/anki/sched.py @@ -1334,7 +1334,7 @@ and (queue=0 or (queue=2 and due<=?))""", def forgetCards(self, ids): "Put cards at the end of the new queue." self.col.db.execute( - "update cards set type=0,queue=0,ivl=0,due=0,factor=? where odid=0 " + "update cards set type=0,queue=0,ivl=0,due=0,odue=0,factor=? where odid=0 " "and queue >= 0 and id in "+ids2str(ids), 2500) pmax = self.col.db.scalar( "select max(due) from cards where type=0") or 0 diff --git a/aqt/main.py b/aqt/main.py index 395346e72..63fee896f 100644 --- a/aqt/main.py +++ b/aqt/main.py @@ -863,6 +863,12 @@ Difference to correct time: %s.""") % diffText def setupHooks(self): addHook("modSchema", self.onSchemaMod) addHook("remNotes", self.onRemNotes) + addHook("odueInvalid", self.onOdueInvalid) + + def onOdueInvalid(self): + showWarning(_("""\ +Invalid property found on card. Please use Tools>Check Database, \ +and if the problem comes up again, please ask on the support site.""")) # Log note deletion ########################################################################## From fb8cb34532298eaf933b5d1d8306381df56ffa25 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Mon, 18 Nov 2013 10:29:40 +0900 Subject: [PATCH 05/15] fix clayout switching back to original tab when flipping --- aqt/clayout.py | 1 + 1 file changed, 1 insertion(+) diff --git a/aqt/clayout.py b/aqt/clayout.py index cec4686de..2d3b858e1 100644 --- a/aqt/clayout.py +++ b/aqt/clayout.py @@ -191,6 +191,7 @@ Please create a new card type first.""")) if self.redrawing: return self.card = self.cards[idx] + self.ord = idx self.tab = self.forms[idx] self.tabs.setCurrentIndex(idx) self.playedAudio = {} From d506515564a29290d5312358fd881abf325679fe Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sat, 23 Nov 2013 18:45:17 +0900 Subject: [PATCH 06/15] wher resizing columns in browser, move others --- designer/browser.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/designer/browser.ui b/designer/browser.ui index 2410f24ea..4adcc979d 100644 --- a/designer/browser.ui +++ b/designer/browser.ui @@ -173,7 +173,7 @@ QAbstractItemView::SelectRows - true + false false From 6ed971cb6b44895050815b829396f0d99afd5801 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 26 Nov 2013 02:43:59 +0900 Subject: [PATCH 07/15] we need to retry when we get BadStatusLine this is caused by the http keep alive being closed by the server --- aqt/sync.py | 85 +++++++++++++++++++++++++++++------------------------ 1 file changed, 46 insertions(+), 39 deletions(-) diff --git a/aqt/sync.py b/aqt/sync.py index 7d85cc7bc..bb0ac2bcb 100644 --- a/aqt/sync.py +++ b/aqt/sync.py @@ -448,46 +448,53 @@ httplib.HTTPConnection.send = _incrementalSend # this is an augmented version of httplib's request routine that: # - doesn't assume requests will be tried more than once # - calls a hook for each chunk of data so we can update the gui +# - retries only when keep-alive connection is closed def _conn_request(self, conn, request_uri, method, body, headers): - try: - if conn.sock is None: - conn.connect() - conn.request(method, request_uri, body, headers) - except socket.timeout: - raise - except socket.gaierror: - conn.close() - raise httplib2.ServerNotFoundError( - "Unable to find the server at %s" % conn.host) - except httplib2.ssl_SSLError: - conn.close() - raise - except socket.error, e: - conn.close() - raise - except httplib.HTTPException: - conn.close() - raise - try: - response = conn.getresponse() - except (socket.error, httplib.HTTPException): - raise - else: - content = "" - if method == "HEAD": - response.close() + for i in range(2): + try: + if conn.sock is None: + conn.connect() + conn.request(method, request_uri, body, headers) + except socket.timeout: + raise + except socket.gaierror: + conn.close() + raise httplib2.ServerNotFoundError( + "Unable to find the server at %s" % conn.host) + except httplib2.ssl_SSLError: + conn.close() + raise + except socket.error, e: + conn.close() + raise + except httplib.HTTPException: + conn.close() + raise + try: + response = conn.getresponse() + except httplib.BadStatusLine: + print "retry bad line" + conn.close() + conn.connect() + continue + except (socket.error, httplib.HTTPException): + raise else: - buf = StringIO() - while 1: - data = response.read(CHUNK_SIZE) - if not data: - break - buf.write(data) - runHook("httpRecv", len(data)) - content = buf.getvalue() - response = httplib2.Response(response) - if method != "HEAD": - content = httplib2._decompressContent(response, content) - return (response, content) + content = "" + if method == "HEAD": + response.close() + else: + buf = StringIO() + while 1: + data = response.read(CHUNK_SIZE) + if not data: + break + buf.write(data) + runHook("httpRecv", len(data)) + content = buf.getvalue() + response = httplib2.Response(response) + if method != "HEAD": + content = httplib2._decompressContent(response, content) + return (response, content) httplib2.Http._conn_request = _conn_request From 99d82c1f2dbb9358a987e2293a78f7fa9ab11ecf Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 26 Nov 2013 03:19:11 +0900 Subject: [PATCH 08/15] fix hasIllegal check, and associated unit test --- anki/media.py | 2 +- tests/test_media.py | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/anki/media.py b/anki/media.py index 822982050..716da58ff 100644 --- a/anki/media.py +++ b/anki/media.py @@ -342,7 +342,7 @@ class MediaManager(object): def hasIllegal(self, str): # a file that couldn't be decoded to unicode is considered invalid if not isinstance(str, unicode): - return False + return True return not not re.search(self._illegalCharReg, str) # Media syncing - bundling zip files to send to server diff --git a/tests/test_media.py b/tests/test_media.py index 4eb673768..e47ac8454 100644 --- a/tests/test_media.py +++ b/tests/test_media.py @@ -1,8 +1,12 @@ # coding: utf-8 -import tempfile, os, time +import tempfile +import os +import time + from shared import getEmptyDeck, testDir + # copying files to media folder def test_add(): d = getEmptyDeck() @@ -100,8 +104,8 @@ def test_changes(): def test_illegal(): d = getEmptyDeck() - aString = "a:b|cd\\e/f\0g*h" - good = "abcdefgh" + aString = u"a:b|cd\\e/f\0g*h" + good = u"abcdefgh" assert d.media.stripIllegal(aString) == good for c in aString: bad = d.media.hasIllegal("somestring"+c+"morestring") From 19b1446758904b7c2bbf50d6eee37eaf737ad4d0 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 26 Nov 2013 03:23:03 +0900 Subject: [PATCH 09/15] automatically remove from filtered deck before reschedule commit 79ed57a44565a6a592f27e239e09e5b37a3f5355 prevented reschedule on cards in a filtered deck, but it is more user friendly to automatically move back to the home deck instead. we also don't need to removeLrn() for review cards, because we're updating type+queue+odue ourselves --- anki/sched.py | 11 ++++++----- tests/test_sched.py | 7 ++++++- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/anki/sched.py b/anki/sched.py index 189dd1582..7e5bcc7ca 100644 --- a/anki/sched.py +++ b/anki/sched.py @@ -1333,9 +1333,10 @@ and (queue=0 or (queue=2 and due<=?))""", 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 odid=0 " - "and queue >= 0 and id in "+ids2str(ids), 2500) + "update cards set type=0,queue=0,ivl=0,due=0,odue=0,factor=?" + " where id in "+ids2str(ids), 2500) pmax = self.col.db.scalar( "select max(due) from cards where type=0") or 0 # takes care of mod + usn @@ -1351,10 +1352,10 @@ and (queue=0 or (queue=2 and due<=?))""", r = random.randint(imin, imax) d.append(dict(id=id, due=r+t, ivl=max(1, r), mod=mod, usn=self.col.usn(), fact=2500)) - self.removeLrn(ids) + self.remFromDyn(ids) self.col.db.executemany(""" -update cards set type=2,queue=2,ivl=:ivl,due=:due, -usn=:usn, mod=:mod, factor=:fact where id=:id and odid=0 and queue >=0""", +update cards set type=2,queue=2,ivl=:ivl,due=:due,odue=0, +usn=:usn,mod=:mod,factor=:fact where id=:id""", d) self.col.log(ids) diff --git a/tests/test_sched.py b/tests/test_sched.py index 0f41d34d4..3073a02c2 100644 --- a/tests/test_sched.py +++ b/tests/test_sched.py @@ -1,10 +1,13 @@ # coding: utf-8 -import time, copy, sys +import time +import copy + from tests.shared import getEmptyDeck from anki.utils import intTime from anki.hooks import addHook + def test_clock(): d = getEmptyDeck() if (d.sched.dayCutoff - intTime()) < 10*60: @@ -173,8 +176,10 @@ def test_learn(): c.queue = 1 c.odue = 321 c.flush() + print "----begin" d.sched.removeLrn() c.load() + print c.__dict__ assert c.queue == 2 assert c.due == 321 From dd1899bcc6b5698ed04f65e38c157f17f3aafcc5 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 26 Nov 2013 04:57:39 +0900 Subject: [PATCH 10/15] cascading column resize must be set after resize --- aqt/browser.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/aqt/browser.py b/aqt/browser.py index 91c4b5644..45f33d554 100644 --- a/aqt/browser.py +++ b/aqt/browser.py @@ -716,6 +716,8 @@ by clicking on one on the left.""")) hh.setResizeMode(i, QHeaderView.Stretch) else: hh.setResizeMode(i, QHeaderView.Interactive) + # this must be set post-resize or it doesn't work + hh.setCascadingSectionResizes(False) def onColumnMoved(self, a, b, c): self.setColumnSizes() From dc2fd097d8c3082cc478a029e8412240fe020ef0 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 26 Nov 2013 17:57:02 +0900 Subject: [PATCH 11/15] if unrecognized url pasted in, paste as text this fixes pasting a url copied from the location bar in chrome --- aqt/editor.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/aqt/editor.py b/aqt/editor.py index 4c34dd1c1..56fd724e9 100644 --- a/aqt/editor.py +++ b/aqt/editor.py @@ -814,7 +814,7 @@ to a cloze type first, via Edit>Change Note Type.""")) for suffix in pics+audio: if l.endswith(suffix): return self._retrieveURL(url) - # not a supported type; return link verbatim + # not a supported type return def isURL(self, s): @@ -1092,11 +1092,11 @@ class EditorWebView(AnkiWebView): self.savedClip = n def _processMime(self, mime): - # print "html=%s image=%s urls=%s txt=%s" % ( - # mime.hasHtml(), mime.hasImage(), mime.hasUrls(), mime.hasText()) - # print "html", mime.html() - # print "urls", mime.urls() - # print "text", mime.text() + print "html=%s image=%s urls=%s txt=%s" % ( + mime.hasHtml(), mime.hasImage(), mime.hasUrls(), mime.hasText()) + print "html", mime.html() + print "urls", mime.urls() + print "text", mime.text() if mime.hasHtml(): return self._processHtml(mime) elif mime.hasUrls(): @@ -1121,6 +1121,8 @@ class EditorWebView(AnkiWebView): link = self.editor.urlToLink(url) if link: mime.setHtml(link) + else: + mime.setText(url) return mime # if the user has used 'copy link location' in the browser, the clipboard From 1f05392113f92fdb8d6adfb3669d4752eff96d1c Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 26 Nov 2013 18:19:54 +0900 Subject: [PATCH 12/15] change default import mode to ignore updates --- aqt/importing.py | 2 +- aqt/profiles.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aqt/importing.py b/aqt/importing.py index 609b08ede..e41bbf1b5 100644 --- a/aqt/importing.py +++ b/aqt/importing.py @@ -79,7 +79,7 @@ class ImportDialog(QDialog): self.onDelimiter) self.updateDelimiterButtonText() self.frm.allowHTML.setChecked(self.mw.pm.profile.get('allowHTML', True)) - self.frm.importMode.setCurrentIndex(self.mw.pm.profile.get('importMode', 0)) + self.frm.importMode.setCurrentIndex(self.mw.pm.profile.get('importMode', 1)) self.exec_() def setupOptions(self): diff --git a/aqt/profiles.py b/aqt/profiles.py index 7c0e6cc74..0b5588447 100644 --- a/aqt/profiles.py +++ b/aqt/profiles.py @@ -56,7 +56,7 @@ profileConf = dict( autoSync=True, # importing allowHTML=False, - importMode=0, + importMode=1, ) class ProfileManager(object): From 0372f30220362b9329cefa8bc88803ae4c4b4e71 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Wed, 27 Nov 2013 19:24:41 +0900 Subject: [PATCH 13/15] adjust media regexp to not trigger on mce_src otherwise pasting the following will cause an error: --- anki/media.py | 4 ++-- aqt/editor.py | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/anki/media.py b/anki/media.py index 716da58ff..d5e6a34e3 100644 --- a/anki/media.py +++ b/anki/media.py @@ -22,9 +22,9 @@ class MediaManager(object): soundRegexps = ["(?i)(\[sound:(?P[^]]+)\])"] imgRegexps = [ # src element quoted case - "(?i)(]+src=(?P[\"'])(?P[^>]+?)(?P=str)[^>]*>)", + "(?i)(]+ src=(?P[\"'])(?P[^>]+?)(?P=str)[^>]*>)", # unquoted case - "(?i)(]+src=(?!['\"])(?P[^ >]+)[^>]*?>)", + "(?i)(]+ src=(?!['\"])(?P[^ >]+)[^>]*?>)", ] regexps = soundRegexps + imgRegexps diff --git a/aqt/editor.py b/aqt/editor.py index 56fd724e9..e347d6d7e 100644 --- a/aqt/editor.py +++ b/aqt/editor.py @@ -1092,11 +1092,11 @@ class EditorWebView(AnkiWebView): self.savedClip = n def _processMime(self, mime): - print "html=%s image=%s urls=%s txt=%s" % ( - mime.hasHtml(), mime.hasImage(), mime.hasUrls(), mime.hasText()) - print "html", mime.html() - print "urls", mime.urls() - print "text", mime.text() + # print "html=%s image=%s urls=%s txt=%s" % ( + # mime.hasHtml(), mime.hasImage(), mime.hasUrls(), mime.hasText()) + # print "html", mime.html() + # print "urls", mime.urls() + # print "text", mime.text() if mime.hasHtml(): return self._processHtml(mime) elif mime.hasUrls(): From 9b02f71abf09db283eabaa451210f7d50301bb62 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Wed, 27 Nov 2013 23:02:03 +0900 Subject: [PATCH 14/15] friendly msg for 10053 error --- aqt/sync.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aqt/sync.py b/aqt/sync.py index bb0ac2bcb..2a38b158b 100644 --- a/aqt/sync.py +++ b/aqt/sync.py @@ -167,7 +167,7 @@ AnkiWeb is too busy at the moment. Please try again in a few minutes.""") return _("504 gateway timeout error received. Please try temporarily disabling your antivirus.") elif "code: 409" in err: return _("Only one client can access AnkiWeb at a time. If a previous sync failed, please try again in a few minutes.") - elif "10061" in err or "10013" in err: + elif "10061" in err or "10013" in err or "10053" in err: return _( "Antivirus or firewall software is preventing Anki from connecting to the internet.") elif "Unable to find the server" in err: From 1c35a590e3ff0260e6db260c57851ac84f637b75 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Fri, 29 Nov 2013 02:07:31 +0900 Subject: [PATCH 15/15] fix regression in unused media check --- anki/media.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/anki/media.py b/anki/media.py index d5e6a34e3..d5afa4b57 100644 --- a/anki/media.py +++ b/anki/media.py @@ -22,9 +22,9 @@ class MediaManager(object): soundRegexps = ["(?i)(\[sound:(?P[^]]+)\])"] imgRegexps = [ # src element quoted case - "(?i)(]+ src=(?P[\"'])(?P[^>]+?)(?P=str)[^>]*>)", + "(?i)(]* src=(?P[\"'])(?P[^>]+?)(?P=str)[^>]*>)", # unquoted case - "(?i)(]+ src=(?!['\"])(?P[^ >]+)[^>]*?>)", + "(?i)(]* src=(?!['\"])(?P[^ >]+)[^>]*?>)", ] regexps = soundRegexps + imgRegexps