mirror of
https://github.com/ankitects/anki.git
synced 2025-11-11 07:07:13 -05:00
Merge branch 'master' of https://github.com/dae/anki
This commit is contained in:
commit
294f177152
9 changed files with 73 additions and 41 deletions
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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, "",
|
||||||
|
|
|
||||||
11
aqt/main.py
11
aqt/main.py
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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);""")
|
||||||
|
|
|
||||||
17
aqt/sync.py
17
aqt/sync.py
|
|
@ -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:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue