Merge branch 'cleanup'

This commit is contained in:
Alex Yatskov 2021-01-18 12:37:03 -08:00
commit abd2fea76f
3 changed files with 197 additions and 184 deletions

View File

@ -1,4 +1,4 @@
# Copyright 2016-2020 Alex Yatskov
# Copyright 2016-2021 Alex Yatskov
#
# 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
@ -32,6 +32,7 @@ import anki
import anki.exporting
import anki.storage
import aqt
from anki.exporting import AnkiPackageExporter
from anki.importing import AnkiPackageImporter
from anki.utils import joinFields, intTime, guid64, fieldChecksum
@ -132,48 +133,48 @@ class AnkiConnect:
reviewer = self.window().reviewer
if reviewer is None:
raise Exception('reviewer is not available')
else:
return reviewer
return reviewer
def collection(self):
collection = self.window().col
if collection is None:
raise Exception('collection is not available')
else:
return collection
return collection
def decks(self):
decks = self.collection().decks
if decks is None:
raise Exception('decks are not available')
else:
return decks
return decks
def scheduler(self):
scheduler = self.collection().sched
if scheduler is None:
raise Exception('scheduler is not available')
else:
return scheduler
return scheduler
def database(self):
database = self.collection().db
if database is None:
raise Exception('database is not available')
else:
return database
return database
def media(self):
media = self.collection().media
if media is None:
raise Exception('media is not available')
else:
return media
return media
def startEditing(self):
@ -209,49 +210,54 @@ class AnkiConnect:
duplicateScope = None
duplicateScopeDeckName = None
duplicateScopeCheckChildren = False
if 'options' in note:
if 'allowDuplicate' in note['options']:
allowDuplicate = note['options']['allowDuplicate']
if type(allowDuplicate) is not bool:
raise Exception('option parameter \'allowDuplicate\' must be boolean')
if 'duplicateScope' in note['options']:
duplicateScope = note['options']['duplicateScope']
if 'duplicateScopeOptions' in note['options']:
duplicateScopeOptions = note['options']['duplicateScopeOptions']
if 'deckName' in duplicateScopeOptions:
duplicateScopeDeckName = duplicateScopeOptions['deckName']
if 'checkChildren' in duplicateScopeOptions:
duplicateScopeCheckChildren = duplicateScopeOptions['checkChildren']
if type(duplicateScopeCheckChildren) is not bool:
raise Exception('option parameter \'duplicateScopeOptions.checkChildren\' must be boolean')
duplicateOrEmpty = self.isNoteDuplicateOrEmptyInScope(ankiNote, deck, collection, duplicateScope, duplicateScopeDeckName, duplicateScopeCheckChildren)
if 'options' in note:
if 'allowDuplicate' in note['options']:
allowDuplicate = note['options']['allowDuplicate']
if type(allowDuplicate) is not bool:
raise Exception('option parameter "allowDuplicate" must be boolean')
if 'duplicateScope' in note['options']:
duplicateScope = note['options']['duplicateScope']
if 'duplicateScopeOptions' in note['options']:
duplicateScopeOptions = note['options']['duplicateScopeOptions']
if 'deckName' in duplicateScopeOptions:
duplicateScopeDeckName = duplicateScopeOptions['deckName']
if 'checkChildren' in duplicateScopeOptions:
duplicateScopeCheckChildren = duplicateScopeOptions['checkChildren']
if type(duplicateScopeCheckChildren) is not bool:
raise Exception('option parameter "duplicateScopeOptions.checkChildren" must be boolean')
duplicateOrEmpty = self.isNoteDuplicateOrEmptyInScope(
ankiNote,
deck,
collection,
duplicateScope,
duplicateScopeDeckName,
duplicateScopeCheckChildren
)
if duplicateOrEmpty == 1:
raise Exception('cannot create note because it is empty')
elif duplicateOrEmpty == 2:
if not allowDuplicate:
if allowDuplicate:
return ankiNote
raise Exception('cannot create note because it is a duplicate')
else:
return ankiNote
elif duplicateOrEmpty == 0:
return ankiNote
else:
raise Exception('cannot create note for unknown reason')
def isNoteDuplicateOrEmptyInScope(self, note, deck, collection, duplicateScope, duplicateScopeDeckName, duplicateScopeCheckChildren):
"1 if first is empty; 2 if first is a duplicate, 0 otherwise."
# 1 if first is empty; 2 if first is a duplicate, 0 otherwise.
if duplicateScope != 'deck':
result = note.dupeOrEmpty()
if result == False:
return 0
return result
return note.dupeOrEmpty() or 0
# dupeOrEmpty returns if a note is a global duplicate
# the rest of the function checks to see if the note is a duplicate in the deck
val = note.fields[0]
if not val.strip():
return 1
csum = anki.utils.fieldChecksum(val)
did = deck['id']
if duplicateScopeDeckName is not None:
@ -261,24 +267,17 @@ class AnkiConnect:
return 0
did = deck2['id']
dids = {}
dids[did] = True
dids = {did: True}
if duplicateScopeCheckChildren:
for kv in collection.decks.children(did):
dids[kv[1]] = True
for noteId in note.col.db.list(
"select id from notes where csum = ? and id != ? and mid = ?",
csum,
note.id or 0,
note.mid,
):
for cardDeckId in note.col.db.list(
"select did from cards where nid = ?",
noteId
):
csum = anki.utils.fieldChecksum(val)
for noteId in note.col.db.list('select id from notes where csum = ? and id != ? and mid = ?', csum, note.id or 0, note.mid):
for cardDeckId in note.col.db.list('select did from cards where nid = ?', noteId):
if cardDeckId in dids:
return 2
return 0
@ -290,23 +289,27 @@ class AnkiConnect:
def version(self):
return util.setting('apiVersion')
@util.api()
def getProfiles(self):
return self.window().pm.profiles()
@util.api()
def loadProfile(self, name):
if name not in self.window().pm.profiles():
return False
if not self.window().isVisible():
self.window().pm.load(name)
self.window().loadProfile()
self.window().profileDiag.closeWithoutQuitting()
else:
if self.window().isVisible():
cur_profile = self.window().pm.name
if cur_profile != name:
self.window().unloadProfileAndShowProfileManager()
self.loadProfile(name)
else:
self.window().pm.load(name)
self.window().loadProfile()
self.window().profileDiag.closeWithoutQuitting()
return True
@ -407,7 +410,7 @@ class AnkiConnect:
@util.api()
def getDeckConfig(self, deck):
if not deck in self.deckNames():
if deck not in self.deckNames():
return False
collection = self.collection()
@ -423,7 +426,7 @@ class AnkiConnect:
config['mod'] = anki.utils.intTime()
config['usn'] = collection.usn()
if not config['id'] in collection.decks.dconf:
if config['id'] not in collection.decks.dconf:
return False
collection.decks.dconf[config['id']] = config
@ -439,7 +442,7 @@ class AnkiConnect:
return False
collection = self.collection()
if not configId in collection.decks.dconf:
if configId not in collection.decks.dconf:
return False
for deck in decks:
@ -452,7 +455,7 @@ class AnkiConnect:
@util.api()
def cloneDeckConfigId(self, name, cloneFrom='1'):
configId = str(cloneFrom)
if not configId in self.collection().decks.dconf:
if configId not in self.collection().decks.dconf:
return False
config = self.collection().decks.getConf(configId)
@ -463,7 +466,7 @@ class AnkiConnect:
def removeDeckConfigId(self, configId):
configId = str(configId)
collection = self.collection()
if configId == 1 or not configId in collection.decks.dconf:
if configId not in collection.decks.dconf:
return False
collection.decks.remConf(configId)
@ -599,6 +602,7 @@ class AnkiConnect:
ankiNote.flush()
@util.api()
def addTags(self, notes, tags, add=True):
self.startEditing()
@ -615,61 +619,60 @@ class AnkiConnect:
def getTags(self):
return self.collection().tags.all()
@util.api()
def clearUnusedTags(self):
self.collection().tags.registerNotes()
self.collection().tags.registerNotes()
@util.api()
def replaceTags(self, notes, tag_to_replace, replace_with_tag):
if self.collection() is not None:
self.window().progress.start()
for nid in notes:
note = self.collection().getNote(nid)
if note.hasTag(tag_to_replace):
note.delTag(tag_to_replace)
note.addtag(replace_with_tag)
note.flush()
self.window().requireReset()
self.window().progress.finish()
self.window().reset()
self.window().progress.start()
for nid in notes:
note = self.collection().getNote(nid)
if note.hasTag(tag_to_replace):
note.delTag(tag_to_replace)
note.addtag(replace_with_tag)
note.flush()
self.window().requireReset()
self.window().progress.finish()
self.window().reset()
@util.api()
def replaceTagsInAllNotes(self, tag_to_replace, replace_with_tag):
collection = self.collection()
if collection is not None:
nids = collection.db.list('select id from notes')
self.window().progress.start()
for nid in nids:
note = collection.getNote(nid)
if note.hasTag(tag_to_replace):
note.delTag(tag_to_replace)
note.addtag(replace_with_tag)
note.flush()
self.window().requireReset()
self.window().progress.finish()
self.window().reset()
return False
self.window().progress.start()
for nid in collection.db.list('select id from notes'):
note = collection.getNote(nid)
if note.hasTag(tag_to_replace):
note.delTag(tag_to_replace)
note.addtag(replace_with_tag)
note.flush()
self.window().requireReset()
self.window().progress.finish()
self.window().reset()
@util.api()
def setEaseFactors(self, cards, easeFactors):
couldSetEaseFactors = []
ind = 0
for card in cards:
for i, card in enumerate(cards):
ankiCard = self.collection().getCard(card)
if ankiCard is None:
raise Exception('card was not found: {}'.format(card['id']))
couldSetEaseFactors.append(False)
else:
couldSetEaseFactors.append(True)
ankiCard.factor = easeFactors[ind]
ankiCard.flush()
ind += 1
return couldSetEaseFactors
@util.api()
def getEaseFactors(self, cards):
easeFactors = []
@ -679,6 +682,7 @@ class AnkiConnect:
return easeFactors
@util.api()
def suspend(self, cards, suspend=True):
for card in cards:
@ -759,11 +763,11 @@ class AnkiConnect:
@util.api()
def createModel(self, modelName, inOrderFields, cardTemplates, css = None):
# https://github.com/dae/anki/blob/b06b70f7214fb1f2ce33ba06d2b095384b81f874/anki/stdmodels.py
if (len(inOrderFields) == 0):
if len(inOrderFields) == 0:
raise Exception('Must provide at least one field for inOrderFields')
if (len(cardTemplates) == 0):
if len(cardTemplates) == 0:
raise Exception('Must provide at least one card for cardTemplates')
if (modelName in self.collection().models.allNames()):
if modelName in self.collection().models.allNames():
raise Exception('Model name already exists')
collection = self.collection()
@ -855,6 +859,7 @@ class AnkiConnect:
return templates
@util.api()
def modelTemplates(self, modelName):
model = self.collection().models.byName(modelName)
@ -885,7 +890,6 @@ class AnkiConnect:
raise Exception('model was not found: {}'.format(model['name']))
templates = model['templates']
for ankiTemplate in ankiModel['tmpls']:
template = templates.get(ankiTemplate['name'])
if template:
@ -919,24 +923,24 @@ class AnkiConnect:
deck = self.collection().decks.get(deckId)
if deck is None:
raise Exception('deck was not found: {}'.format(deckId))
else:
return deck['name']
return deck['name']
@util.api()
def findNotes(self, query=None):
if query is None:
return []
else:
return list(map(int, self.collection().findNotes(query)))
return list(map(int, self.collection().findNotes(query)))
@util.api()
def findCards(self, query=None):
if query is None:
return []
else:
return list(map(int, self.collection().findCards(query)))
return list(map(int, self.collection().findCards(query)))
@util.api()
@ -957,8 +961,8 @@ class AnkiConnect:
'cardId': card.id,
'fields': fields,
'fieldOrder': card.ord,
'question': util.getQuestion(card),
'answer': util.getAnswer(card),
'question': util.cardQuestion(card),
'answer': util.cardAnswer(card),
'modelName': model['name'],
'ord': card.ord,
'deckName': self.deckNameFromId(card.did),
@ -987,65 +991,74 @@ class AnkiConnect:
@util.api()
def cardReviews(self, deck, startID):
return self.database().all("select id, cid, usn, ease, ivl, lastIvl, factor, time, type from revlog "
"where id>? and cid in (select id from cards where did=?)",
startID, self.decks().id(deck))
return self.database().all(
'select id, cid, usn, ease, ivl, lastIvl, factor, time, type from revlog ''where id>? and cid in (select id from cards where did=?)',
startID,
self.decks().id(deck)
)
@util.api()
def reloadCollection(self):
self.collection().reset()
@util.api()
def getLatestReviewID(self, deck):
return self.database().scalar("select max(id) from revlog where cid in (select id from cards where did=?)",
self.decks().id(deck)) or 0
return self.database().scalar(
'select max(id) from revlog where cid in (select id from cards where did=?)',
self.decks().id(deck)
) or 0
@util.api()
def updateCompleteDeck(self, data):
self.startEditing()
did = self.decks().id(data["deck"])
did = self.decks().id(data['deck'])
self.decks().flush()
model_manager = self.collection().models
for _, card in data["cards"].items():
for _, card in data['cards'].items():
self.database().execute(
"replace into cards (id, nid, did, ord, type, queue, due, ivl, factor, reps, lapses, left, "
"mod, usn, odue, odid, flags, data) "
"values (" + "?," * (12 + 6 - 1) + "?)",
card["id"], card["nid"], did, card["ord"], card["type"], card["queue"], card["due"],
card["ivl"], card["factor"], card["reps"], card["lapses"], card["left"],
'replace into cards (id, nid, did, ord, type, queue, due, ivl, factor, reps, lapses, left, '
'mod, usn, odue, odid, flags, data) '
'values (' + '?,' * (12 + 6 - 1) + '?)',
card['id'], card['nid'], did, card['ord'], card['type'], card['queue'], card['due'],
card['ivl'], card['factor'], card['reps'], card['lapses'], card['left'],
intTime(), -1, 0, 0, 0, 0
)
note = data["notes"][str(card["nid"])]
tags = self.collection().tags.join(self.collection().tags.canonify(note["tags"]))
note = data['notes'][str(card['nid'])]
tags = self.collection().tags.join(self.collection().tags.canonify(note['tags']))
self.database().execute(
"replace into notes(id, mid, tags, flds,"
"guid, mod, usn, flags, data, sfld, csum) values (" + "?," * (4 + 7 - 1) + "?)",
note["id"], note["mid"], tags, joinFields(note["fields"]),
guid64(), intTime(), -1, 0, 0, "", fieldChecksum(note["fields"][0])
'replace into notes(id, mid, tags, flds,'
'guid, mod, usn, flags, data, sfld, csum) values (' + '?,' * (4 + 7 - 1) + '?)',
note['id'], note['mid'], tags, joinFields(note['fields']),
guid64(), intTime(), -1, 0, 0, '', fieldChecksum(note['fields'][0])
)
model = data["models"][str(note["mid"])]
if not model_manager.get(model["id"]):
model_o = model_manager.new(model["name"])
for field_name in model["fields"]:
model = data['models'][str(note['mid'])]
if not model_manager.get(model['id']):
model_o = model_manager.new(model['name'])
for field_name in model['fields']:
field = model_manager.newField(field_name)
model_manager.addField(model_o, field)
for template_name in model["templateNames"]:
for template_name in model['templateNames']:
template = model_manager.newTemplate(template_name)
model_manager.addTemplate(model_o, template)
model_o["id"] = model["id"]
model_o['id'] = model['id']
model_manager.update(model_o)
model_manager.flush()
self.stopEditing()
@util.api()
def insertReviews(self, reviews):
if len(reviews) == 0: return
sql = "insert into revlog(id,cid,usn,ease,ivl,lastIvl,factor,time,type) values "
for row in reviews:
sql += "(%s)," % ",".join(map(str, row))
sql = sql[:-1]
self.database().execute(sql)
if len(reviews) > 0:
sql = 'insert into revlog(id,cid,usn,ease,ivl,lastIvl,factor,time,type) values '
for row in reviews:
sql += '(%s),' % ','.join(map(str, row))
sql = sql[:-1]
self.database().execute(sql)
@util.api()
def notesInfo(self, notes):
@ -1085,6 +1098,7 @@ class AnkiConnect:
finally:
self.stopEditing()
@util.api()
def removeEmptyNotes(self):
for model in self.collection().models.all():
@ -1115,7 +1129,6 @@ class AnkiConnect:
@util.api()
def guiAddCards(self, note=None):
if note is not None:
collection = self.collection()
@ -1123,15 +1136,15 @@ class AnkiConnect:
if deck is None:
raise Exception('deck was not found: {}'.format(note['deckName']))
self.collection().decks.select(deck['id'])
collection.decks.select(deck['id'])
savedMid = deck.pop('mid', None)
model = collection.models.byName(note['modelName'])
if model is None:
raise Exception('model was not found: {}'.format(note['modelName']))
self.collection().models.setCurrent(model)
self.collection().models.update(model)
collection.models.setCurrent(model)
collection.models.update(model)
closeAfterAdding = False
if note is not None and 'options' in note:
@ -1156,8 +1169,8 @@ class AnkiConnect:
self.modelHasChanged = True
super().__init__(mw)
self.addButton.setText("Add and Close")
self.addButton.setShortcut(aqt.qt.QKeySequence("Ctrl+Return"))
self.addButton.setText('Add and Close')
self.addButton.setShortcut(aqt.qt.QKeySequence('Ctrl+Return'))
def _addCards(self):
super()._addCards()
@ -1259,6 +1272,7 @@ class AnkiConnect:
return addCards.editor.note.id
@util.api()
def guiReviewActive(self):
return self.reviewer().card is not None and self.window().state == 'review'
@ -1279,21 +1293,21 @@ class AnkiConnect:
order = info['ord']
name = info['name']
fields[name] = {'value': note.fields[order], 'order': order}
if card is not None:
buttonList = reviewer._answerButtonList()
return {
'cardId': card.id,
'fields': fields,
'fieldOrder': card.ord,
'question': util.getQuestion(card),
'answer': util.getAnswer(card),
'buttons': [b[0] for b in buttonList],
'nextReviews': [reviewer.mw.col.sched.nextIvlStr(reviewer.card, b[0], True) for b in buttonList],
'modelName': model['name'],
'deckName': self.deckNameFromId(card.did),
'css': model['css'],
'template': card.template()['name']
}
buttonList = reviewer._answerButtonList()
return {
'cardId': card.id,
'fields': fields,
'fieldOrder': card.ord,
'question': util.cardQuestion(card),
'answer': util.cardAnswer(card),
'buttons': [b[0] for b in buttonList],
'nextReviews': [reviewer.mw.col.sched.nextIvlStr(reviewer.card, b[0], True) for b in buttonList],
'modelName': model['name'],
'deckName': self.deckNameFromId(card.did),
'css': model['css'],
'template': card.template()['name']
}
@util.api()
@ -1302,12 +1316,11 @@ class AnkiConnect:
return False
card = self.reviewer().card
if card is not None:
card.startTimer()
return True
else:
return False
return False
@util.api()
@ -1315,8 +1328,8 @@ class AnkiConnect:
if self.guiReviewActive():
self.reviewer()._showQuestion()
return True
else:
return False
return False
@util.api()
@ -1324,8 +1337,8 @@ class AnkiConnect:
if self.guiReviewActive():
self.window().reviewer._showAnswer()
return True
else:
return False
return False
@util.api()
@ -1366,8 +1379,8 @@ class AnkiConnect:
if self.guiDeckOverview(name):
self.window().moveToState('review')
return True
else:
return False
return False
@util.api()
@ -1383,7 +1396,7 @@ class AnkiConnect:
for note in notes:
try:
results.append(self.addNote(note))
except Exception:
except:
results.append(None)
return results
@ -1409,8 +1422,10 @@ class AnkiConnect:
exporter.includeSched = includeSched
exporter.exportInto(path)
return True
return False
@util.api()
def importPackage(self, path):
collection = self.collection()
@ -1432,4 +1447,4 @@ class AnkiConnect:
# Entry
#
ac = AnkiConnect()
ac = AnkiConnect()

View File

@ -1,4 +1,4 @@
# Copyright 2016-2020 Alex Yatskov
# Copyright 2016-2021 Alex Yatskov
#
# 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
@ -24,11 +24,12 @@ import enum
# Utilities
#
class MediaType (enum.Enum):
class MediaType(enum.Enum):
Audio = 1
Video = 2
Picture = 3
def download(url):
client = anki.sync.AnkiRequestsClient()
client.timeout = setting('webTimeout') / 1000
@ -49,20 +50,18 @@ def api(*versions):
return decorator
def getQuestion(card):
def cardQuestion(card):
if getattr(card, 'question', None) is None:
question = card._getQA()['q']
else:
question = card.question(),
return question
return card._getQA()['q']
return card.question(),
def getAnswer(card):
def cardAnswer(card):
if getattr(card, 'answer', None) is None:
answer = card._getQA()['a']
else:
answer = card.answer()
return answer
return card._getQA()['a']
return card.answer()
def setting(key):

View File

@ -1,4 +1,4 @@
# Copyright 2016-2020 Alex Yatskov
# Copyright 2016-2021 Alex Yatskov
#
# 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
@ -17,7 +17,7 @@ import json
import select
import socket
from . import web, util
from . import util
#
# WebRequest
@ -210,4 +210,3 @@ class WebServer:
client.close()
self.clients = []