This commit is contained in:
Soren I. Bjornstad 2013-11-13 10:34:51 -06:00
commit 294f177152
9 changed files with 73 additions and 41 deletions

View file

@ -51,9 +51,8 @@ defaultConf = {
# this is initialized by storage.Collection # this is initialized by storage.Collection
class _Collection(object): class _Collection(object):
debugLog = False def __init__(self, db, server=False, log=False):
self._debugLog = log
def __init__(self, db, server=False):
self.db = db self.db = db
self.path = db._path self.path = db._path
self._openLog() self._openLog()
@ -695,8 +694,12 @@ select id from notes where mid not in """ + ids2str(self.models.ids()))
self.remNotes(ids) self.remNotes(ids)
# for each model # for each model
for m in self.models.all(): for m in self.models.all():
# cards with invalid ordinal
if m['type'] == MODEL_STD: 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(""" ids = self.db.list("""
select id from cards where ord not in %s and nid in ( select id from cards where ord not in %s and nid in (
select id from notes where mid = ?)""" % select id from notes where mid = ?)""" %
@ -779,7 +782,7 @@ and queue = 0""", intTime(), self.usn())
########################################################################## ##########################################################################
def log(self, *args, **kwargs): def log(self, *args, **kwargs):
if not self.debugLog: if not self._debugLog:
return return
def customRepr(x): def customRepr(x):
if isinstance(x, basestring): if isinstance(x, basestring):
@ -794,9 +797,14 @@ and queue = 0""", intTime(), self.usn())
print buf print buf
def _openLog(self): def _openLog(self):
if not self.debugLog: if not self._debugLog:
return return
lpath = re.sub("\.anki2$", ".log", self.path) lpath = re.sub("\.anki2$", ".log", self.path)
if os.path.exists(lpath) and os.path.getsize(lpath) > 10*1024*1024:
lpath2 = lpath + ".old"
if os.path.exists(lpath2):
os.unlink(lpath2)
os.rename(lpath, lpath2)
self._logHnd = open(lpath, "ab") self._logHnd = open(lpath, "ab")
def _closeLog(self): def _closeLog(self):

View file

@ -213,6 +213,7 @@ class MediaManager(object):
allRefs.update(noteRefs) allRefs.update(noteRefs)
# loop through media folder # loop through media folder
unused = [] unused = []
invalid = []
if local is None: if local is None:
files = os.listdir(mdir) files = os.listdir(mdir)
else: else:
@ -225,6 +226,9 @@ class MediaManager(object):
if file.startswith("_"): if file.startswith("_"):
# leading _ says to ignore file # leading _ says to ignore file
continue continue
if not isinstance(file, unicode):
invalid.append(unicode(file, sys.getfilesystemencoding(), "replace"))
continue
nfcFile = unicodedata.normalize("NFC", file) nfcFile = unicodedata.normalize("NFC", file)
# we enforce NFC fs encoding on non-macs; on macs we'll have gotten # we enforce NFC fs encoding on non-macs; on macs we'll have gotten
# NFD so we use the above variable for comparing references # NFD so we use the above variable for comparing references
@ -242,7 +246,7 @@ class MediaManager(object):
else: else:
allRefs.discard(nfcFile) allRefs.discard(nfcFile)
nohave = [x for x in allRefs if not x.startswith("_")] nohave = [x for x in allRefs if not x.startswith("_")]
return (nohave, unused) return (nohave, unused, invalid)
def _normalizeNoteRefs(self, nid): def _normalizeNoteRefs(self, nid):
note = self.col.getNote(nid) note = self.col.getNote(nid)
@ -336,6 +340,9 @@ class MediaManager(object):
return re.sub(self._illegalCharReg, "", str) return re.sub(self._illegalCharReg, "", str)
def hasIllegal(self, str): def hasIllegal(self, str):
# a file that couldn't be decoded to unicode is considered invalid
if not isinstance(str, unicode):
return False
return not not re.search(self._illegalCharReg, str) return not not re.search(self._illegalCharReg, str)
# Media syncing - bundling zip files to send to server # Media syncing - bundling zip files to send to server

View file

@ -1279,6 +1279,7 @@ To study outside of the normal schedule, click the Custom Study button below."""
def buryCards(self, cids): def buryCards(self, cids):
self.col.log(cids) self.col.log(cids)
self.remFromDyn(cids)
self.removeLrn(cids) self.removeLrn(cids)
self.col.db.execute(""" self.col.db.execute("""
update cards set queue=-2,mod=?,usn=? where id in """+ids2str(cids), update cards set queue=-2,mod=?,usn=? where id in """+ids2str(cids),
@ -1414,7 +1415,6 @@ and due >= ? and queue = 0""" % scids, now, self.col.usn(), shiftby, low)
d.append(dict(now=now, due=due[nid], usn=self.col.usn(), cid=id)) d.append(dict(now=now, due=due[nid], usn=self.col.usn(), cid=id))
self.col.db.executemany( self.col.db.executemany(
"update cards set due=:due,mod=:now,usn=:usn where id = :cid", d) "update cards set due=:due,mod=:now,usn=:usn where id = :cid", d)
self.col.log(cids)
def randomizeCards(self, did): def randomizeCards(self, did):
cids = self.col.db.list("select id from cards where did = ?", did) cids = self.col.db.list("select id from cards where did = ?", did)

View file

@ -2,7 +2,10 @@
# Copyright: Damien Elmes <anki@ichi2.net> # Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import os, copy, re import os
import copy
import re
from anki.lang import _ from anki.lang import _
from anki.utils import intTime, json from anki.utils import intTime, json
from anki.db import DB from anki.db import DB
@ -11,7 +14,8 @@ from anki.consts import *
from anki.stdmodels import addBasicModel, addClozeModel, addForwardReverse, \ from anki.stdmodels import addBasicModel, addClozeModel, addForwardReverse, \
addForwardOptionalReverse addForwardOptionalReverse
def Collection(path, lock=True, server=False, sync=True):
def Collection(path, lock=True, server=False, sync=True, log=False):
"Open a new or existing collection. Path must be unicode." "Open a new or existing collection. Path must be unicode."
assert path.endswith(".anki2") assert path.endswith(".anki2")
path = os.path.abspath(path) path = os.path.abspath(path)
@ -33,7 +37,7 @@ def Collection(path, lock=True, server=False, sync=True):
else: else:
db.execute("pragma synchronous = off") db.execute("pragma synchronous = off")
# add db to col and do any remaining upgrades # add db to col and do any remaining upgrades
col = _Collection(db, server) col = _Collection(db, server, log)
if ver < SCHEMA_VERSION: if ver < SCHEMA_VERSION:
_upgrade(col, ver) _upgrade(col, ver)
elif create: elif create:

View file

@ -752,6 +752,7 @@ class MediaSyncer(object):
# back from sanity check to addFiles # back from sanity check to addFiles
s = self.server.mediaSanity() s = self.server.mediaSanity()
c = self.mediaSanity() c = self.mediaSanity()
self.col.log("mediaSanity", c, s)
if c != s: if c != s:
# if the sanity check failed, force a resync # if the sanity check failed, force a resync
self.col.media.forceResync() self.col.media.forceResync()

View file

@ -1,10 +1,14 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright: Damien Elmes <anki@ichi2.net> # Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from anki.lang import _ import re
import os
import urllib2
import ctypes
import urllib
from anki.lang import _
from aqt.qt import * from aqt.qt import *
import re, os, urllib2, ctypes
from anki.utils import stripHTML, isWin, isMac, namedtmp, json, stripHTMLMedia from anki.utils import stripHTML, isWin, isMac, namedtmp, json, stripHTMLMedia
import anki.sound import anki.sound
from anki.hooks import runHook, runFilter from anki.hooks import runHook, runFilter
@ -15,7 +19,6 @@ from aqt.utils import shortcut, showInfo, showWarning, getBase, getFile, \
import aqt import aqt
import anki.js import anki.js
from BeautifulSoup import BeautifulSoup from BeautifulSoup import BeautifulSoup
import urllib
pics = ("jpg", "jpeg", "png", "tif", "tiff", "gif", "svg") pics = ("jpg", "jpeg", "png", "tif", "tiff", "gif", "svg")
audio = ("wav", "mp3", "ogg", "flac", "mp4", "swf", "mov", "mpeg", "mkv") audio = ("wav", "mp3", "ogg", "flac", "mp4", "swf", "mov", "mpeg", "mkv")
@ -313,6 +316,7 @@ class Editor(object):
b.setFixedHeight(20) b.setFixedHeight(20)
b.setFixedWidth(20) b.setFixedWidth(20)
if not native: if not native:
if self.plastiqueStyle:
b.setStyle(self.plastiqueStyle) b.setStyle(self.plastiqueStyle)
b.setFocusPolicy(Qt.NoFocus) b.setFocusPolicy(Qt.NoFocus)
else: else:
@ -333,18 +337,22 @@ class Editor(object):
def setupButtons(self): def setupButtons(self):
self._buttons = {} self._buttons = {}
# button styles for mac # button styles for mac
if not isMac:
self.plastiqueStyle = QStyleFactory.create("plastique") self.plastiqueStyle = QStyleFactory.create("plastique")
if not self.plastiqueStyle: if not self.plastiqueStyle:
# plastique was removed in qt5 # plastique was removed in qt5
self.plastiqueStyle = QStyleFactory.create("fusion") self.plastiqueStyle = QStyleFactory.create("fusion")
self.widget.setStyle(self.plastiqueStyle) self.widget.setStyle(self.plastiqueStyle)
else:
self.plastiqueStyle = None
# icons # icons
self.iconsBox = QHBoxLayout() self.iconsBox = QHBoxLayout()
if not isMac: if not isMac:
self.iconsBox.setMargin(6) self.iconsBox.setMargin(6)
self.iconsBox.setSpacing(0)
else: else:
self.iconsBox.setMargin(0) self.iconsBox.setMargin(0)
self.iconsBox.setSpacing(0) self.iconsBox.setSpacing(14)
self.outerLayout.addLayout(self.iconsBox) self.outerLayout.addLayout(self.iconsBox)
b = self._addButton b = self._addButton
b("fields", self.onFields, "", b("fields", self.onFields, "",

View file

@ -31,8 +31,6 @@ class AnkiQt(QMainWindow):
self.state = "startup" self.state = "startup"
aqt.mw = self aqt.mw = self
self.app = app self.app = app
from anki.collection import _Collection
_Collection.debugLog = True
if isWin: if isWin:
self._xpstyle = QStyleFactory.create("WindowsXP") self._xpstyle = QStyleFactory.create("WindowsXP")
self.app.setStyle(self._xpstyle) self.app.setStyle(self._xpstyle)
@ -270,7 +268,7 @@ To import into a password protected profile, please open the profile before atte
def loadCollection(self): def loadCollection(self):
self.hideSchemaMsg = True self.hideSchemaMsg = True
try: try:
self.col = Collection(self.pm.collectionPath()) self.col = Collection(self.pm.collectionPath(), log=True)
except anki.db.Error: except anki.db.Error:
# move back to profile manager # move back to profile manager
showWarning("""\ showWarning("""\
@ -915,11 +913,16 @@ will be lost. Continue?"""))
def onCheckMediaDB(self): def onCheckMediaDB(self):
self.progress.start(immediate=True) self.progress.start(immediate=True)
(nohave, unused) = self.col.media.check() (nohave, unused, invalid) = self.col.media.check()
self.progress.finish() self.progress.finish()
# generate report # generate report
report = "" report = ""
if invalid:
report += _("Invalid encoding; please rename:")
report += "\n" + "\n".join(invalid)
if unused: if unused:
if report:
report += "\n\n\n"
report += _( report += _(
"In media folder but not used by any cards:") "In media folder but not used by any cards:")
report += "\n" + "\n".join(unused) report += "\n" + "\n".join(unused)

View file

@ -6,8 +6,13 @@
# - Saves in pickles rather than json to easily store Qt window state. # - Saves in pickles rather than json to easily store Qt window state.
# - Saves in sqlite rather than a flat file so the config can't be corrupted # - Saves in sqlite rather than a flat file so the config can't be corrupted
import os
import random
import cPickle
import locale
import re
from aqt.qt import * from aqt.qt import *
import os, random, cPickle, shutil, locale, re
from anki.db import DB from anki.db import DB
from anki.utils import isMac, isWin, intTime, checksum from anki.utils import isMac, isWin, intTime, checksum
from anki.lang import langs from anki.lang import langs
@ -16,6 +21,7 @@ from aqt import appHelpSite
import aqt.forms import aqt.forms
from send2trash import send2trash from send2trash import send2trash
metaConf = dict( metaConf = dict(
ver=0, ver=0,
updates=True, updates=True,
@ -186,7 +192,6 @@ documentation for information on using a flash drive.""")
def _loadMeta(self): def _loadMeta(self):
path = os.path.join(self.base, "prefs.db") path = os.path.join(self.base, "prefs.db")
new = not os.path.exists(path) new = not os.path.exists(path)
self.db = DB(path, text=str)
def recover(): def recover():
# if we can't load profile, start with a new one # if we can't load profile, start with a new one
os.rename(path, path+".broken") os.rename(path, path+".broken")
@ -195,6 +200,7 @@ documentation for information on using a flash drive.""")
Anki's prefs.db file was corrupt and has been recreated. If you were using multiple \ Anki's prefs.db file was corrupt and has been recreated. If you were using multiple \
profiles, please add them back using the same names to recover your cards.""") profiles, please add them back using the same names to recover your cards.""")
try: try:
self.db = DB(path, text=str)
self.db.execute(""" self.db.execute("""
create table if not exists profiles create table if not exists profiles
(name text primary key, data text not null);""") (name text primary key, data text not null);""")

View file

@ -280,7 +280,7 @@ class SyncThread(QThread):
self.syncMsg = "" self.syncMsg = ""
self.uname = "" self.uname = ""
try: try:
self.col = Collection(self.path) self.col = Collection(self.path, log=True)
except: except:
self.fireEvent("corrupt") self.fireEvent("corrupt")
return return
@ -421,7 +421,7 @@ class SyncThread(QThread):
###################################################################### ######################################################################
CHUNK_SIZE = 65536 CHUNK_SIZE = 65536
import httplib, httplib2, errno import httplib, httplib2
from cStringIO import StringIO from cStringIO import StringIO
from anki.hooks import runHook from anki.hooks import runHook
@ -448,6 +448,9 @@ def _incrementalSend(self, data):
httplib.HTTPConnection.send = _incrementalSend httplib.HTTPConnection.send = _incrementalSend
# receiving in httplib2 # receiving in httplib2
# 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
def _conn_request(self, conn, request_uri, method, body, headers): def _conn_request(self, conn, request_uri, method, body, headers):
try: try:
if conn.sock is None: if conn.sock is None:
@ -463,17 +466,9 @@ def _conn_request(self, conn, request_uri, method, body, headers):
conn.close() conn.close()
raise raise
except socket.error, e: except socket.error, e:
err = 0 conn.close()
if hasattr(e, 'args'):
err = getattr(e, 'args')[0]
else:
err = e.errno
if err == errno.ECONNREFUSED: # Connection refused
raise raise
except httplib.HTTPException: except httplib.HTTPException:
# Just because the server closed the connection doesn't apparently mean
# that the server didn't send a response.
if conn.sock is None:
conn.close() conn.close()
raise raise
try: try: