mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 14:32:22 -04:00
hook up new note and note type handling
- notetypes are fetched from the DB as needed, and cached in Python - handle note type changes in the backend. Multiple operations can now be performed in one go, but this is not currently exposed in the GUI. - extra methods to grab sorted note type names quickly, and fetch by name - col.models.save() without a provided notetype is now a no-op - note loading/saving handled in the backend - notes with no valid cards can now be added - templates can now be deleted even if they would previously orphan notes a number of fixmes have been left in notes.py and models.py
This commit is contained in:
parent
8b0121b0ac
commit
f637ac957d
14 changed files with 365 additions and 403 deletions
|
@ -166,8 +166,6 @@ decks from col"""
|
||||||
)
|
)
|
||||||
self.decks.decks = self.backend.get_all_decks()
|
self.decks.decks = self.backend.get_all_decks()
|
||||||
self.decks.changed = False
|
self.decks.changed = False
|
||||||
self.models.models = self.backend.get_all_notetypes()
|
|
||||||
self.models.changed = False
|
|
||||||
|
|
||||||
def setMod(self) -> None:
|
def setMod(self) -> None:
|
||||||
"""Mark DB modified.
|
"""Mark DB modified.
|
||||||
|
@ -232,6 +230,7 @@ crt=?, mod=?, scm=?, dty=?, usn=?, ls=?""",
|
||||||
self.save(trx=False)
|
self.save(trx=False)
|
||||||
else:
|
else:
|
||||||
self.db.rollback()
|
self.db.rollback()
|
||||||
|
self.models._clear_cache()
|
||||||
self.backend.close_collection(downgrade=downgrade)
|
self.backend.close_collection(downgrade=downgrade)
|
||||||
self.db = None
|
self.db = None
|
||||||
self.media.close()
|
self.media.close()
|
||||||
|
@ -319,21 +318,12 @@ crt=?, mod=?, scm=?, dty=?, usn=?, ls=?""",
|
||||||
"Return a new note with the current model."
|
"Return a new note with the current model."
|
||||||
return Note(self, self.models.current(forDeck))
|
return Note(self, self.models.current(forDeck))
|
||||||
|
|
||||||
|
def add_note(self, note: Note, deck_id: int) -> None:
|
||||||
|
note.id = self.backend.add_note(note.to_backend_note(), deck_id)
|
||||||
|
|
||||||
def addNote(self, note: Note) -> int:
|
def addNote(self, note: Note) -> int:
|
||||||
"""Add a note to the collection. Return number of new cards."""
|
self.add_note(note, note.model()["did"])
|
||||||
# check we have card models available, then save
|
return len(note.cards())
|
||||||
cms = self.findTemplates(note)
|
|
||||||
if not cms:
|
|
||||||
return 0
|
|
||||||
note.flush()
|
|
||||||
# deck conf governs which of these are used
|
|
||||||
due = self.nextID("pos")
|
|
||||||
# add cards
|
|
||||||
ncards = 0
|
|
||||||
for template in cms:
|
|
||||||
self._newCard(note, template, due)
|
|
||||||
ncards += 1
|
|
||||||
return ncards
|
|
||||||
|
|
||||||
def remNotes(self, ids: Iterable[int]) -> None:
|
def remNotes(self, ids: Iterable[int]) -> None:
|
||||||
"""Deletes notes with the given IDs."""
|
"""Deletes notes with the given IDs."""
|
||||||
|
|
|
@ -226,7 +226,7 @@ class AnkiExporter(Exporter):
|
||||||
# need to reset card state
|
# need to reset card state
|
||||||
self.dst.sched.resetCards(cids)
|
self.dst.sched.resetCards(cids)
|
||||||
# models - start with zero
|
# models - start with zero
|
||||||
self.dst.models.models = {}
|
self.dst.models.remove_all_notetypes()
|
||||||
for m in self.src.models.all():
|
for m in self.src.models.all():
|
||||||
if int(m["id"]) in mids:
|
if int(m["id"]) in mids:
|
||||||
self.dst.models.update(m)
|
self.dst.models.update(m)
|
||||||
|
|
|
@ -10,7 +10,7 @@ import time
|
||||||
import urllib.error
|
import urllib.error
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import urllib.request
|
import urllib.request
|
||||||
from typing import Any, Callable, List, Optional, Tuple, Union
|
from typing import Any, Callable, List, Optional, Tuple
|
||||||
|
|
||||||
import anki
|
import anki
|
||||||
from anki.consts import *
|
from anki.consts import *
|
||||||
|
@ -122,7 +122,7 @@ class MediaManager:
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def filesInStr(
|
def filesInStr(
|
||||||
self, mid: Union[int, str], string: str, includeRemote: bool = False
|
self, mid: int, string: str, includeRemote: bool = False
|
||||||
) -> List[str]:
|
) -> List[str]:
|
||||||
l = []
|
l = []
|
||||||
model = self.col.models.get(mid)
|
model = self.col.models.get(mid)
|
||||||
|
|
|
@ -6,9 +6,10 @@ from __future__ import annotations
|
||||||
import copy
|
import copy
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||||
|
|
||||||
import anki # pylint: disable=unused-import
|
import anki # pylint: disable=unused-import
|
||||||
|
import anki.backend_pb2 as pb
|
||||||
from anki import hooks
|
from anki import hooks
|
||||||
from anki.consts import *
|
from anki.consts import *
|
||||||
from anki.lang import _
|
from anki.lang import _
|
||||||
|
@ -23,12 +24,16 @@ TemplateRequirementType = str # Union["all", "any", "none"]
|
||||||
TemplateRequiredFieldOrds = Tuple[int, TemplateRequirementType, List[int]]
|
TemplateRequiredFieldOrds = Tuple[int, TemplateRequirementType, List[int]]
|
||||||
AllTemplateReqs = List[TemplateRequiredFieldOrds]
|
AllTemplateReqs = List[TemplateRequiredFieldOrds]
|
||||||
|
|
||||||
|
# fixme: memory leaks
|
||||||
|
# fixme: syncing, beforeUpload
|
||||||
|
|
||||||
# Models
|
# Models
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
# - careful not to add any lists/dicts/etc here, as they aren't deep copied
|
# - careful not to add any lists/dicts/etc here, as they aren't deep copied
|
||||||
|
|
||||||
defaultModel: NoteType = {
|
defaultModel: NoteType = {
|
||||||
|
"id": 0,
|
||||||
"sortf": 0,
|
"sortf": 0,
|
||||||
"did": 1,
|
"did": 1,
|
||||||
"latexPre": """\
|
"latexPre": """\
|
||||||
|
@ -43,7 +48,7 @@ defaultModel: NoteType = {
|
||||||
"latexPost": "\\end{document}",
|
"latexPost": "\\end{document}",
|
||||||
"mod": 0,
|
"mod": 0,
|
||||||
"usn": 0,
|
"usn": 0,
|
||||||
"vers": [], # FIXME: remove when other clients have caught up
|
"req": [],
|
||||||
"type": MODEL_STD,
|
"type": MODEL_STD,
|
||||||
"css": """\
|
"css": """\
|
||||||
.card {
|
.card {
|
||||||
|
@ -83,50 +88,131 @@ defaultTemplate: Template = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ModelManager:
|
class ModelsDictProxy:
|
||||||
models: Dict[str, NoteType]
|
def __init__(self, col: anki.storage._Collection):
|
||||||
|
self._col = col.weakref()
|
||||||
|
|
||||||
|
def _warn(self):
|
||||||
|
print("add-on should use methods on col.models, not col.models.models dict")
|
||||||
|
|
||||||
|
def __getitem__(self, item):
|
||||||
|
self._warn()
|
||||||
|
return self._col.models.get(int(item))
|
||||||
|
|
||||||
|
def __setitem__(self, key, val):
|
||||||
|
self._warn()
|
||||||
|
self._col.models.save(val)
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
self._warn()
|
||||||
|
return len(self._col.models.all_names_and_ids())
|
||||||
|
|
||||||
|
def keys(self):
|
||||||
|
self._warn()
|
||||||
|
return [str(nt.id) for nt in self._col.models.all_names_and_ids()]
|
||||||
|
|
||||||
|
def values(self):
|
||||||
|
self._warn()
|
||||||
|
return self._col.models.all()
|
||||||
|
|
||||||
|
def items(self):
|
||||||
|
self._warn()
|
||||||
|
return [(str(nt["id"]), nt) for nt in self._col.models.all()]
|
||||||
|
|
||||||
|
def __contains__(self, item):
|
||||||
|
self._warn()
|
||||||
|
self._col.models.have(item)
|
||||||
|
|
||||||
|
|
||||||
|
class ModelManager:
|
||||||
# Saving/loading registry
|
# Saving/loading registry
|
||||||
#############################################################
|
#############################################################
|
||||||
|
|
||||||
def __init__(self, col: anki.storage._Collection) -> None:
|
def __init__(self, col: anki.storage._Collection) -> None:
|
||||||
self.col = col.weakref()
|
self.col = col.weakref()
|
||||||
self.models = {}
|
self.models = ModelsDictProxy(col)
|
||||||
self.changed = False
|
# do not access this directly!
|
||||||
|
self._cache = {}
|
||||||
|
|
||||||
def save(
|
def save(
|
||||||
self,
|
self,
|
||||||
m: Optional[NoteType] = None,
|
m: NoteType = None,
|
||||||
|
# no longer used
|
||||||
templates: bool = False,
|
templates: bool = False,
|
||||||
updateReqs: bool = True,
|
updateReqs: bool = True,
|
||||||
) -> None:
|
) -> None:
|
||||||
"Mark M modified if provided, and schedule registry flush."
|
"Save changes made to provided note type."
|
||||||
if m and m["id"]:
|
if not m:
|
||||||
m["mod"] = intTime()
|
print("col.models.save() should be passed the changed notetype")
|
||||||
m["usn"] = self.col.usn()
|
return
|
||||||
if updateReqs:
|
|
||||||
self._updateRequired(m)
|
self.update(m, preserve_usn=False)
|
||||||
if templates:
|
|
||||||
self._syncTemplates(m)
|
# fixme: badly named; also fires on updates
|
||||||
self.changed = True
|
|
||||||
hooks.note_type_added(m)
|
hooks.note_type_added(m)
|
||||||
|
|
||||||
|
# legacy
|
||||||
def flush(self) -> None:
|
def flush(self) -> None:
|
||||||
"Flush the registry if any models were changed."
|
pass
|
||||||
if self.changed:
|
|
||||||
self.ensureNotEmpty()
|
|
||||||
self.col.backend.set_all_notetypes(self.models)
|
|
||||||
self.changed = False
|
|
||||||
|
|
||||||
|
# fixme: enforce at lower level
|
||||||
def ensureNotEmpty(self) -> Optional[bool]:
|
def ensureNotEmpty(self) -> Optional[bool]:
|
||||||
if not self.models:
|
if not self.all_names_and_ids():
|
||||||
from anki.stdmodels import addBasicModel
|
from anki.stdmodels import addBasicModel
|
||||||
|
|
||||||
addBasicModel(self.col)
|
addBasicModel(self.col)
|
||||||
return True
|
return True
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Retrieving and creating models
|
# Caching
|
||||||
|
#############################################################
|
||||||
|
# A lot of existing code expects to be able to quickly and
|
||||||
|
# frequently obtain access to an entire notetype, so we currently
|
||||||
|
# need to cache responses from the backend. Please do not
|
||||||
|
# access the cache directly!
|
||||||
|
|
||||||
|
_cache: Dict[int, NoteType] = {}
|
||||||
|
|
||||||
|
def _update_cache(self, nt: NoteType) -> None:
|
||||||
|
self._cache[nt["id"]] = nt
|
||||||
|
|
||||||
|
def _remove_from_cache(self, ntid: int) -> None:
|
||||||
|
if ntid in self._cache:
|
||||||
|
del self._cache[ntid]
|
||||||
|
|
||||||
|
def _get_cached(self, ntid: int) -> Optional[NoteType]:
|
||||||
|
return self._cache.get(ntid)
|
||||||
|
|
||||||
|
def _clear_cache(self):
|
||||||
|
self._cache = {}
|
||||||
|
|
||||||
|
# Listing note types
|
||||||
|
#############################################################
|
||||||
|
|
||||||
|
def all_names_and_ids(self) -> List[pb.NoteTypeNameID]:
|
||||||
|
return self.col.backend.get_notetype_names_and_ids()
|
||||||
|
|
||||||
|
def all_use_counts(self) -> List[pb.NoteTypeNameIDUseCount]:
|
||||||
|
return self.col.backend.get_notetype_use_counts()
|
||||||
|
|
||||||
|
def id_for_name(self, name: str) -> Optional[int]:
|
||||||
|
return self.col.backend.get_notetype_id_by_name(name)
|
||||||
|
|
||||||
|
# legacy
|
||||||
|
|
||||||
|
def allNames(self) -> List[str]:
|
||||||
|
return [n.name for n in self.all_names_and_ids()]
|
||||||
|
|
||||||
|
def ids(self) -> List[int]:
|
||||||
|
return [n.id for n in self.all_names_and_ids()]
|
||||||
|
|
||||||
|
# only used by importing code
|
||||||
|
def have(self, id: int) -> bool:
|
||||||
|
if isinstance(id, str):
|
||||||
|
id = int(id)
|
||||||
|
return any(True for e in self.all_names_and_ids() if e.id == id)
|
||||||
|
|
||||||
|
# Current note type
|
||||||
#############################################################
|
#############################################################
|
||||||
|
|
||||||
def current(self, forDeck: bool = True) -> Any:
|
def current(self, forDeck: bool = True) -> Any:
|
||||||
|
@ -134,33 +220,46 @@ class ModelManager:
|
||||||
m = self.get(self.col.decks.current().get("mid"))
|
m = self.get(self.col.decks.current().get("mid"))
|
||||||
if not forDeck or not m:
|
if not forDeck or not m:
|
||||||
m = self.get(self.col.conf["curModel"])
|
m = self.get(self.col.conf["curModel"])
|
||||||
return m or list(self.models.values())[0]
|
if m:
|
||||||
|
return m
|
||||||
|
return self.get(self.all_names_and_ids()[0].id)
|
||||||
|
|
||||||
def setCurrent(self, m: NoteType) -> None:
|
def setCurrent(self, m: NoteType) -> None:
|
||||||
self.col.conf["curModel"] = m["id"]
|
self.col.conf["curModel"] = m["id"]
|
||||||
self.col.setMod()
|
self.col.setMod()
|
||||||
|
|
||||||
def get(self, id: Any) -> Any:
|
# Retrieving and creating models
|
||||||
|
#############################################################
|
||||||
|
|
||||||
|
def get(self, id: int) -> Optional[NoteType]:
|
||||||
"Get model with ID, or None."
|
"Get model with ID, or None."
|
||||||
id = str(id)
|
# deal with various legacy input types
|
||||||
if id in self.models:
|
if id is None:
|
||||||
return self.models[id]
|
return None
|
||||||
|
elif isinstance(id, str):
|
||||||
|
id = int(id)
|
||||||
|
|
||||||
def all(self) -> List:
|
nt = self._get_cached(id)
|
||||||
|
if not nt:
|
||||||
|
nt = self.col.backend.get_notetype_legacy(id)
|
||||||
|
if nt:
|
||||||
|
self._update_cache(nt)
|
||||||
|
return nt
|
||||||
|
|
||||||
|
def all(self) -> List[NoteType]:
|
||||||
"Get all models."
|
"Get all models."
|
||||||
return list(self.models.values())
|
return [self.get(nt.id) for nt in self.all_names_and_ids()]
|
||||||
|
|
||||||
def allNames(self) -> List:
|
def byName(self, name: str) -> Optional[NoteType]:
|
||||||
return [m["name"] for m in self.all()]
|
|
||||||
|
|
||||||
def byName(self, name: str) -> Any:
|
|
||||||
"Get model with NAME."
|
"Get model with NAME."
|
||||||
for m in list(self.models.values()):
|
id = self.id_for_name(name)
|
||||||
if m["name"] == name:
|
if id:
|
||||||
return m
|
return self.get(id)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
def new(self, name: str) -> NoteType:
|
def new(self, name: str) -> NoteType:
|
||||||
"Create a new model, save it in the registry, and return it."
|
"Create a new model, and return it."
|
||||||
# caller should call save() after modifying
|
# caller should call save() after modifying
|
||||||
m = defaultModel.copy()
|
m = defaultModel.copy()
|
||||||
m["name"] = name
|
m["name"] = name
|
||||||
|
@ -168,59 +267,50 @@ class ModelManager:
|
||||||
m["flds"] = []
|
m["flds"] = []
|
||||||
m["tmpls"] = []
|
m["tmpls"] = []
|
||||||
m["tags"] = []
|
m["tags"] = []
|
||||||
m["id"] = None
|
m["id"] = 0
|
||||||
return m
|
return m
|
||||||
|
|
||||||
def rem(self, m: NoteType) -> None:
|
def rem(self, m: NoteType) -> None:
|
||||||
"Delete model, and all its cards/notes."
|
"Delete model, and all its cards/notes."
|
||||||
|
self.remove(m["id"])
|
||||||
|
|
||||||
|
def remove_all_notetypes(self):
|
||||||
self.col.modSchema(check=True)
|
self.col.modSchema(check=True)
|
||||||
current = self.current()["id"] == m["id"]
|
for nt in self.all_names_and_ids():
|
||||||
# delete notes/cards
|
self._remove_from_cache(nt.id)
|
||||||
self.col.remCards(
|
self.col.backend.remove_notetype(nt.id)
|
||||||
self.col.db.list(
|
|
||||||
"""
|
def remove(self, id: int) -> None:
|
||||||
select id from cards where nid in (select id from notes where mid = ?)""",
|
self.col.modSchema(check=True)
|
||||||
m["id"],
|
self._remove_from_cache(id)
|
||||||
)
|
was_current = self.current()["id"] == id
|
||||||
)
|
self.col.backend.remove_notetype(id)
|
||||||
# then the model
|
|
||||||
del self.models[str(m["id"])]
|
# fixme: handle in backend
|
||||||
self.save()
|
if was_current:
|
||||||
# GUI should ensure last model is not deleted
|
self.col.conf["curModel"] = self.all_names_and_ids()[0].id
|
||||||
if current:
|
|
||||||
self.setCurrent(list(self.models.values())[0])
|
|
||||||
|
|
||||||
def add(self, m: NoteType) -> None:
|
def add(self, m: NoteType) -> None:
|
||||||
self._setID(m)
|
|
||||||
self.update(m)
|
|
||||||
self.setCurrent(m)
|
|
||||||
self.save(m)
|
self.save(m)
|
||||||
|
|
||||||
def ensureNameUnique(self, m: NoteType) -> None:
|
def ensureNameUnique(self, m: NoteType) -> None:
|
||||||
for mcur in self.all():
|
existing_id = self.id_for_name(m["name"])
|
||||||
if mcur["name"] == m["name"] and mcur["id"] != m["id"]:
|
if existing_id is not None and existing_id != m["id"]:
|
||||||
m["name"] += "-" + checksum(str(time.time()))[:5]
|
m["name"] += "-" + checksum(str(time.time()))[:5]
|
||||||
break
|
|
||||||
|
|
||||||
def update(self, m: NoteType) -> None:
|
def update(self, m: NoteType, preserve_usn=True) -> None:
|
||||||
"Add or update an existing model. Used for syncing and merging."
|
"Add or update an existing model. Use .save() instead."
|
||||||
|
self._remove_from_cache(m["id"])
|
||||||
self.ensureNameUnique(m)
|
self.ensureNameUnique(m)
|
||||||
self.models[str(m["id"])] = m
|
self.col.backend.add_or_update_notetype(m, preserve_usn=preserve_usn)
|
||||||
# mark registry changed, but don't bump mod time
|
self.setCurrent(m)
|
||||||
self.save()
|
self._mutate_after_write(m)
|
||||||
|
|
||||||
def _setID(self, m: NoteType) -> None:
|
def _mutate_after_write(self, nt: NoteType) -> None:
|
||||||
while 1:
|
# existing code expects the note type to be mutated to reflect
|
||||||
id = str(intTime(1000))
|
# the changes made when adding, such as ordinal assignment :-(
|
||||||
if id not in self.models:
|
updated = self.get(nt["id"])
|
||||||
break
|
nt.update(updated)
|
||||||
m["id"] = id
|
|
||||||
|
|
||||||
def have(self, id: int) -> bool:
|
|
||||||
return str(id) in self.models
|
|
||||||
|
|
||||||
def ids(self) -> List[str]:
|
|
||||||
return list(self.models.keys())
|
|
||||||
|
|
||||||
# Tools
|
# Tools
|
||||||
##################################################
|
##################################################
|
||||||
|
@ -231,17 +321,9 @@ select id from cards where nid in (select id from notes where mid = ?)""",
|
||||||
|
|
||||||
def useCount(self, m: NoteType) -> Any:
|
def useCount(self, m: NoteType) -> Any:
|
||||||
"Number of note using M."
|
"Number of note using M."
|
||||||
|
print("useCount() is slow; prefer all_use_counts()")
|
||||||
return self.col.db.scalar("select count() from notes where mid = ?", m["id"])
|
return self.col.db.scalar("select count() from notes where mid = ?", m["id"])
|
||||||
|
|
||||||
def tmplUseCount(self, m: NoteType, ord) -> Any:
|
|
||||||
return self.col.db.scalar(
|
|
||||||
"""
|
|
||||||
select count() from cards, notes where cards.nid = notes.id
|
|
||||||
and notes.mid = ? and cards.ord = ?""",
|
|
||||||
m["id"],
|
|
||||||
ord,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Copying
|
# Copying
|
||||||
##################################################
|
##################################################
|
||||||
|
|
||||||
|
@ -249,18 +331,13 @@ and notes.mid = ? and cards.ord = ?""",
|
||||||
"Copy, save and return."
|
"Copy, save and return."
|
||||||
m2 = copy.deepcopy(m)
|
m2 = copy.deepcopy(m)
|
||||||
m2["name"] = _("%s copy") % m2["name"]
|
m2["name"] = _("%s copy") % m2["name"]
|
||||||
|
m2["id"] = 0
|
||||||
self.add(m2)
|
self.add(m2)
|
||||||
return m2
|
return m2
|
||||||
|
|
||||||
# Fields
|
# Fields
|
||||||
##################################################
|
##################################################
|
||||||
|
|
||||||
def newField(self, name: str) -> Field:
|
|
||||||
assert isinstance(name, str)
|
|
||||||
f = defaultField.copy()
|
|
||||||
f["name"] = name
|
|
||||||
return f
|
|
||||||
|
|
||||||
def fieldMap(self, m: NoteType) -> Dict[str, Tuple[int, Field]]:
|
def fieldMap(self, m: NoteType) -> Dict[str, Tuple[int, Field]]:
|
||||||
"Mapping of field name -> (ord, field)."
|
"Mapping of field name -> (ord, field)."
|
||||||
return dict((f["name"], (f["ord"], f)) for f in m["flds"])
|
return dict((f["name"], (f["ord"], f)) for f in m["flds"])
|
||||||
|
@ -274,111 +351,55 @@ and notes.mid = ? and cards.ord = ?""",
|
||||||
def setSortIdx(self, m: NoteType, idx: int) -> None:
|
def setSortIdx(self, m: NoteType, idx: int) -> None:
|
||||||
assert 0 <= idx < len(m["flds"])
|
assert 0 <= idx < len(m["flds"])
|
||||||
self.col.modSchema(check=True)
|
self.col.modSchema(check=True)
|
||||||
m["sortf"] = idx
|
|
||||||
self.col.updateFieldCache(self.nids(m))
|
|
||||||
self.save(m, updateReqs=False)
|
|
||||||
|
|
||||||
def addField(self, m: NoteType, field: Field) -> None:
|
m["sortf"] = idx
|
||||||
# only mod schema if model isn't new
|
|
||||||
if m["id"]:
|
|
||||||
self.col.modSchema(check=True)
|
|
||||||
m["flds"].append(field)
|
|
||||||
self._updateFieldOrds(m)
|
|
||||||
self.save(m)
|
self.save(m)
|
||||||
|
|
||||||
def add(fields):
|
# Adding & changing fields
|
||||||
fields.append("")
|
##################################################
|
||||||
return fields
|
|
||||||
|
|
||||||
self._transformFields(m, add)
|
def newField(self, name: str) -> Field:
|
||||||
|
assert isinstance(name, str)
|
||||||
|
f = defaultField.copy()
|
||||||
|
f["name"] = name
|
||||||
|
return f
|
||||||
|
|
||||||
|
def addField(self, m: NoteType, field: Field) -> None:
|
||||||
|
if m["id"]:
|
||||||
|
self.col.modSchema(check=True)
|
||||||
|
|
||||||
|
m["flds"].append(field)
|
||||||
|
|
||||||
|
if m["id"]:
|
||||||
|
self.save(m)
|
||||||
|
|
||||||
def remField(self, m: NoteType, field: Field) -> None:
|
def remField(self, m: NoteType, field: Field) -> None:
|
||||||
self.col.modSchema(check=True)
|
self.col.modSchema(check=True)
|
||||||
# save old sort field
|
|
||||||
sortFldName = m["flds"][m["sortf"]]["name"]
|
|
||||||
idx = m["flds"].index(field)
|
|
||||||
m["flds"].remove(field)
|
m["flds"].remove(field)
|
||||||
# restore old sort field if possible, or revert to first field
|
|
||||||
m["sortf"] = 0
|
|
||||||
for c, f in enumerate(m["flds"]):
|
|
||||||
if f["name"] == sortFldName:
|
|
||||||
m["sortf"] = c
|
|
||||||
break
|
|
||||||
self._updateFieldOrds(m)
|
|
||||||
|
|
||||||
def delete(fields):
|
self.save(m)
|
||||||
del fields[idx]
|
|
||||||
return fields
|
|
||||||
|
|
||||||
self._transformFields(m, delete)
|
|
||||||
if m["flds"][m["sortf"]]["name"] != sortFldName:
|
|
||||||
# need to rebuild sort field
|
|
||||||
self.col.updateFieldCache(self.nids(m))
|
|
||||||
# saves
|
|
||||||
self.renameField(m, field, None)
|
|
||||||
|
|
||||||
def moveField(self, m: NoteType, field: Field, idx: int) -> None:
|
def moveField(self, m: NoteType, field: Field, idx: int) -> None:
|
||||||
self.col.modSchema(check=True)
|
self.col.modSchema(check=True)
|
||||||
oldidx = m["flds"].index(field)
|
oldidx = m["flds"].index(field)
|
||||||
if oldidx == idx:
|
if oldidx == idx:
|
||||||
return
|
return
|
||||||
# remember old sort field
|
|
||||||
sortf = m["flds"][m["sortf"]]
|
|
||||||
# move
|
|
||||||
m["flds"].remove(field)
|
m["flds"].remove(field)
|
||||||
m["flds"].insert(idx, field)
|
m["flds"].insert(idx, field)
|
||||||
# restore sort field
|
|
||||||
m["sortf"] = m["flds"].index(sortf)
|
|
||||||
self._updateFieldOrds(m)
|
|
||||||
self.save(m, updateReqs=False)
|
|
||||||
|
|
||||||
def move(fields, oldidx=oldidx):
|
|
||||||
val = fields[oldidx]
|
|
||||||
del fields[oldidx]
|
|
||||||
fields.insert(idx, val)
|
|
||||||
return fields
|
|
||||||
|
|
||||||
self._transformFields(m, move)
|
|
||||||
|
|
||||||
def renameField(self, m: NoteType, field: Field, newName: Optional[str]) -> None:
|
|
||||||
self.col.modSchema(check=True)
|
|
||||||
if newName is not None:
|
|
||||||
newName = newName.replace(":", "")
|
|
||||||
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)
|
self.save(m)
|
||||||
|
|
||||||
def _updateFieldOrds(self, m: NoteType) -> None:
|
def renameField(self, m: NoteType, field: Field, newName: str) -> None:
|
||||||
for c, f in enumerate(m["flds"]):
|
assert field in m["flds"]
|
||||||
f["ord"] = c
|
|
||||||
|
|
||||||
def _transformFields(self, m: NoteType, fn: Callable) -> None:
|
field["name"] = newName
|
||||||
# model hasn't been added yet?
|
|
||||||
if not m["id"]:
|
|
||||||
return
|
|
||||||
r = []
|
|
||||||
for (id, flds) in self.col.db.execute(
|
|
||||||
"select id, flds from notes where mid = ?", m["id"]
|
|
||||||
):
|
|
||||||
r.append((joinFields(fn(splitFields(flds))), intTime(), self.col.usn(), id))
|
|
||||||
self.col.db.executemany("update notes set flds=?,mod=?,usn=? where id = ?", r)
|
|
||||||
|
|
||||||
# Templates
|
self.save(m)
|
||||||
|
|
||||||
|
# Adding & changing templates
|
||||||
##################################################
|
##################################################
|
||||||
|
|
||||||
def newTemplate(self, name: str) -> Template:
|
def newTemplate(self, name: str) -> Template:
|
||||||
|
@ -387,84 +408,33 @@ and notes.mid = ? and cards.ord = ?""",
|
||||||
return t
|
return t
|
||||||
|
|
||||||
def addTemplate(self, m: NoteType, template: Template) -> None:
|
def addTemplate(self, m: NoteType, template: Template) -> None:
|
||||||
"Note: should col.genCards() afterwards."
|
|
||||||
if m["id"]:
|
if m["id"]:
|
||||||
self.col.modSchema(check=True)
|
self.col.modSchema(check=True)
|
||||||
|
|
||||||
m["tmpls"].append(template)
|
m["tmpls"].append(template)
|
||||||
self._updateTemplOrds(m)
|
|
||||||
self.save(m)
|
|
||||||
|
|
||||||
def remTemplate(self, m: NoteType, template: Template) -> bool:
|
if m["id"]:
|
||||||
"False if removing template would leave orphan notes."
|
self.save(m)
|
||||||
|
|
||||||
|
def remTemplate(self, m: NoteType, template: Template) -> None:
|
||||||
assert len(m["tmpls"]) > 1
|
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.modSchema(check=True)
|
||||||
self.col.remCards(cids)
|
|
||||||
# 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
|
|
||||||
|
|
||||||
def _updateTemplOrds(self, m: NoteType) -> None:
|
m["tmpls"].remove(template)
|
||||||
for c, t in enumerate(m["tmpls"]):
|
|
||||||
t["ord"] = c
|
self.save(m)
|
||||||
|
|
||||||
def moveTemplate(self, m: NoteType, template: Template, idx: int) -> None:
|
def moveTemplate(self, m: NoteType, template: Template, idx: int) -> None:
|
||||||
|
self.col.modSchema(check=True)
|
||||||
|
|
||||||
oldidx = m["tmpls"].index(template)
|
oldidx = m["tmpls"].index(template)
|
||||||
if oldidx == idx:
|
if oldidx == idx:
|
||||||
return
|
return
|
||||||
oldidxs = dict((id(t), t["ord"]) for t in m["tmpls"])
|
|
||||||
m["tmpls"].remove(template)
|
m["tmpls"].remove(template)
|
||||||
m["tmpls"].insert(idx, template)
|
m["tmpls"].insert(idx, template)
|
||||||
self._updateTemplOrds(m)
|
|
||||||
# generate change map
|
|
||||||
map = []
|
|
||||||
for t in m["tmpls"]:
|
|
||||||
map.append("when ord = %d then %d" % (oldidxs[id(t)], t["ord"]))
|
|
||||||
# apply
|
|
||||||
self.save(m, updateReqs=False)
|
|
||||||
self.col.db.execute(
|
|
||||||
"""
|
|
||||||
update cards set ord = (case %s end),usn=?,mod=? where nid in (
|
|
||||||
select id from notes where mid = ?)"""
|
|
||||||
% " ".join(map),
|
|
||||||
self.col.usn(),
|
|
||||||
intTime(),
|
|
||||||
m["id"],
|
|
||||||
)
|
|
||||||
|
|
||||||
def _syncTemplates(self, m: NoteType) -> None:
|
self.save(m)
|
||||||
rem = self.col.genCards(self.nids(m))
|
|
||||||
|
|
||||||
# Model changing
|
# Model changing
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
|
@ -3,35 +3,19 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, List, Optional, Tuple
|
||||||
|
|
||||||
import anki # pylint: disable=unused-import
|
import anki # pylint: disable=unused-import
|
||||||
from anki import hooks
|
from anki import hooks
|
||||||
from anki.models import Field, NoteType
|
from anki.models import NoteType
|
||||||
from anki.utils import (
|
from anki.rsbackend import BackendNote
|
||||||
fieldChecksum,
|
from anki.utils import fieldChecksum, joinFields, splitFields, stripHTMLMedia
|
||||||
guid64,
|
|
||||||
intTime,
|
|
||||||
joinFields,
|
|
||||||
splitFields,
|
|
||||||
stripHTMLMedia,
|
|
||||||
timestampID,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Note:
|
class Note:
|
||||||
col: anki.storage._Collection
|
# not currently exposed
|
||||||
newlyAdded: bool
|
flags = 0
|
||||||
id: int
|
data = ""
|
||||||
guid: str
|
|
||||||
_model: NoteType
|
|
||||||
mid: int
|
|
||||||
tags: List[str]
|
|
||||||
fields: List[str]
|
|
||||||
flags: int
|
|
||||||
data: str
|
|
||||||
_fmap: Dict[str, Tuple[int, Field]]
|
|
||||||
scm: int
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
@ -41,78 +25,51 @@ class Note:
|
||||||
) -> None:
|
) -> None:
|
||||||
assert not (model and id)
|
assert not (model and id)
|
||||||
self.col = col.weakref()
|
self.col = col.weakref()
|
||||||
self.newlyAdded = False
|
# self.newlyAdded = False
|
||||||
|
|
||||||
if id:
|
if id:
|
||||||
|
# existing note
|
||||||
self.id = id
|
self.id = id
|
||||||
self.load()
|
self.load()
|
||||||
else:
|
else:
|
||||||
self.id = timestampID(col.db, "notes")
|
# new note for provided notetype
|
||||||
self.guid = guid64()
|
self._load_from_backend_note(self.col.backend.new_note(model["id"]))
|
||||||
self._model = model
|
|
||||||
self.mid = model["id"]
|
|
||||||
self.tags = []
|
|
||||||
self.fields = [""] * len(self._model["flds"])
|
|
||||||
self.flags = 0
|
|
||||||
self.data = ""
|
|
||||||
self._fmap = self.col.models.fieldMap(self._model)
|
|
||||||
self.scm = self.col.scm
|
|
||||||
|
|
||||||
def load(self) -> None:
|
def load(self) -> None:
|
||||||
(
|
n = self.col.backend.get_note(self.id)
|
||||||
self.guid,
|
assert n
|
||||||
self.mid,
|
self._load_from_backend_note(n)
|
||||||
self.mod,
|
|
||||||
self.usn,
|
def _load_from_backend_note(self, n: BackendNote) -> None:
|
||||||
tags,
|
self.id = n.id
|
||||||
fields,
|
self.guid = n.guid
|
||||||
self.flags,
|
self.mid = n.ntid
|
||||||
self.data,
|
self.mod = n.mtime_secs
|
||||||
) = self.col.db.first(
|
self.usn = n.usn
|
||||||
"""
|
self.tags = list(n.tags)
|
||||||
select guid, mid, mod, usn, tags, flds, flags, data
|
self.fields = list(n.fields)
|
||||||
from notes where id = ?""",
|
|
||||||
self.id,
|
|
||||||
)
|
|
||||||
self.fields = splitFields(fields)
|
|
||||||
self.tags = self.col.tags.split(tags)
|
|
||||||
self._model = self.col.models.get(self.mid)
|
self._model = self.col.models.get(self.mid)
|
||||||
self._fmap = self.col.models.fieldMap(self._model)
|
self._fmap = self.col.models.fieldMap(self._model)
|
||||||
self.scm = self.col.scm
|
|
||||||
|
|
||||||
def flush(self, mod: Optional[int] = None) -> None:
|
# fixme: only save tags in list on save
|
||||||
"If fields or tags have changed, write changes to disk."
|
def to_backend_note(self) -> BackendNote:
|
||||||
assert self.scm == self.col.scm
|
hooks.note_will_flush(self)
|
||||||
self._preFlush()
|
return BackendNote(
|
||||||
sfld = stripHTMLMedia(self.fields[self.col.models.sortIdx(self._model)])
|
id=self.id,
|
||||||
tags = self.stringTags()
|
guid=self.guid,
|
||||||
fields = self.joinedFields()
|
ntid=self.mid,
|
||||||
if not mod and self.col.db.scalar(
|
mtime_secs=self.mod,
|
||||||
"select 1 from notes where id = ? and tags = ? and flds = ?",
|
usn=self.usn,
|
||||||
self.id,
|
# fixme: catch spaces in individual tags
|
||||||
tags,
|
tags=" ".join(self.tags).split(" "),
|
||||||
fields,
|
fields=self.fields,
|
||||||
):
|
|
||||||
return
|
|
||||||
csum = fieldChecksum(self.fields[0])
|
|
||||||
self.mod = mod if mod else intTime()
|
|
||||||
self.usn = self.col.usn()
|
|
||||||
res = self.col.db.execute(
|
|
||||||
"""
|
|
||||||
insert or replace into notes values (?,?,?,?,?,?,?,?,?,?,?)""",
|
|
||||||
self.id,
|
|
||||||
self.guid,
|
|
||||||
self.mid,
|
|
||||||
self.mod,
|
|
||||||
self.usn,
|
|
||||||
tags,
|
|
||||||
fields,
|
|
||||||
sfld,
|
|
||||||
csum,
|
|
||||||
self.flags,
|
|
||||||
self.data,
|
|
||||||
)
|
)
|
||||||
self.col.tags.register(self.tags)
|
|
||||||
self._postFlush()
|
def flush(self, mod=None) -> None:
|
||||||
|
# fixme: mod unused?
|
||||||
|
assert self.id != 0
|
||||||
|
self.col.backend.update_note(self.to_backend_note())
|
||||||
|
|
||||||
def joinedFields(self) -> str:
|
def joinedFields(self) -> str:
|
||||||
return joinFields(self.fields)
|
return joinFields(self.fields)
|
||||||
|
@ -198,22 +155,3 @@ insert or replace into notes values (?,?,?,?,?,?,?,?,?,?,?)""",
|
||||||
if stripHTMLMedia(splitFields(flds)[0]) == stripHTMLMedia(self.fields[0]):
|
if stripHTMLMedia(splitFields(flds)[0]) == stripHTMLMedia(self.fields[0]):
|
||||||
return 2
|
return 2
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Flushing cloze notes
|
|
||||||
##################################################
|
|
||||||
|
|
||||||
def _preFlush(self) -> None:
|
|
||||||
hooks.note_will_flush(self)
|
|
||||||
# have we been added yet?
|
|
||||||
self.newlyAdded = not self.col.db.scalar(
|
|
||||||
"select 1 from cards where nid = ?", self.id
|
|
||||||
)
|
|
||||||
|
|
||||||
def _postFlush(self) -> None:
|
|
||||||
# generate missing cards
|
|
||||||
if not self.newlyAdded:
|
|
||||||
rem = 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
|
|
||||||
# self.col.remEmptyCards(ids)
|
|
||||||
|
|
|
@ -47,6 +47,7 @@ assert ankirspy.buildhash() == anki.buildinfo.buildhash
|
||||||
SchedTimingToday = pb.SchedTimingTodayOut
|
SchedTimingToday = pb.SchedTimingTodayOut
|
||||||
BuiltinSortKind = pb.BuiltinSearchOrder.BuiltinSortKind
|
BuiltinSortKind = pb.BuiltinSearchOrder.BuiltinSortKind
|
||||||
BackendCard = pb.Card
|
BackendCard = pb.Card
|
||||||
|
BackendNote = pb.Note
|
||||||
TagUsnTuple = pb.TagUsnTuple
|
TagUsnTuple = pb.TagUsnTuple
|
||||||
NoteType = pb.NoteType
|
NoteType = pb.NoteType
|
||||||
|
|
||||||
|
@ -98,6 +99,10 @@ class TemplateError(StringError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NotFoundError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def proto_exception_to_native(err: pb.BackendError) -> Exception:
|
def proto_exception_to_native(err: pb.BackendError) -> Exception:
|
||||||
val = err.WhichOneof("value")
|
val = err.WhichOneof("value")
|
||||||
if val == "interrupted":
|
if val == "interrupted":
|
||||||
|
@ -116,6 +121,8 @@ def proto_exception_to_native(err: pb.BackendError) -> Exception:
|
||||||
return StringError(err.localized)
|
return StringError(err.localized)
|
||||||
elif val == "json_error":
|
elif val == "json_error":
|
||||||
return StringError(err.localized)
|
return StringError(err.localized)
|
||||||
|
elif val == "not_found_error":
|
||||||
|
return NotFoundError()
|
||||||
else:
|
else:
|
||||||
assert_impossible_literal(val)
|
assert_impossible_literal(val)
|
||||||
|
|
||||||
|
@ -609,15 +616,12 @@ class RustBackend:
|
||||||
def set_all_config(self, conf: Dict[str, Any]):
|
def set_all_config(self, conf: Dict[str, Any]):
|
||||||
self._run_command(pb.BackendInput(set_all_config=orjson.dumps(conf)))
|
self._run_command(pb.BackendInput(set_all_config=orjson.dumps(conf)))
|
||||||
|
|
||||||
def get_all_notetypes(self) -> Dict[str, Dict[str, Any]]:
|
def get_changed_notetypes(self, usn: int) -> Dict[str, Dict[str, Any]]:
|
||||||
jstr = self._run_command(
|
jstr = self._run_command(
|
||||||
pb.BackendInput(get_all_notetypes=pb.Empty())
|
pb.BackendInput(get_changed_notetypes=usn)
|
||||||
).get_all_notetypes
|
).get_changed_notetypes
|
||||||
return orjson.loads(jstr)
|
return orjson.loads(jstr)
|
||||||
|
|
||||||
def set_all_notetypes(self, nts: Dict[str, Dict[str, Any]]):
|
|
||||||
self._run_command(pb.BackendInput(set_all_notetypes=orjson.dumps(nts)))
|
|
||||||
|
|
||||||
def get_all_decks(self) -> Dict[str, Dict[str, Any]]:
|
def get_all_decks(self) -> Dict[str, Dict[str, Any]]:
|
||||||
jstr = self._run_command(
|
jstr = self._run_command(
|
||||||
pb.BackendInput(get_all_decks=pb.Empty())
|
pb.BackendInput(get_all_decks=pb.Empty())
|
||||||
|
@ -634,6 +638,67 @@ class RustBackend:
|
||||||
).all_stock_notetypes.notetypes
|
).all_stock_notetypes.notetypes
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_notetype_names_and_ids(self) -> List[pb.NoteTypeNameID]:
|
||||||
|
return list(
|
||||||
|
self._run_command(
|
||||||
|
pb.BackendInput(get_notetype_names=pb.Empty())
|
||||||
|
).get_notetype_names.entries
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_notetype_use_counts(self) -> List[pb.NoteTypeNameIDUseCount]:
|
||||||
|
return list(
|
||||||
|
self._run_command(
|
||||||
|
pb.BackendInput(get_notetype_names_and_counts=pb.Empty())
|
||||||
|
).get_notetype_names_and_counts.entries
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_notetype_legacy(self, ntid: int) -> Optional[Dict]:
|
||||||
|
try:
|
||||||
|
bytes = self._run_command(
|
||||||
|
pb.BackendInput(get_notetype_legacy=ntid)
|
||||||
|
).get_notetype_legacy
|
||||||
|
except NotFoundError:
|
||||||
|
return None
|
||||||
|
return orjson.loads(bytes)
|
||||||
|
|
||||||
|
def get_notetype_id_by_name(self, name: str) -> Optional[int]:
|
||||||
|
return (
|
||||||
|
self._run_command(
|
||||||
|
pb.BackendInput(get_notetype_id_by_name=name)
|
||||||
|
).get_notetype_id_by_name
|
||||||
|
or None
|
||||||
|
)
|
||||||
|
|
||||||
|
def add_or_update_notetype(self, nt: Dict[str, Any], preserve_usn: bool) -> None:
|
||||||
|
bjson = orjson.dumps(nt)
|
||||||
|
id = self._run_command(
|
||||||
|
pb.BackendInput(
|
||||||
|
add_or_update_notetype=pb.AddOrUpdateNotetypeIn(
|
||||||
|
json=bjson, preserve_usn_and_mtime=preserve_usn
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).add_or_update_notetype
|
||||||
|
nt["id"] = id
|
||||||
|
|
||||||
|
def remove_notetype(self, ntid: int) -> None:
|
||||||
|
self._run_command(pb.BackendInput(remove_notetype=ntid))
|
||||||
|
|
||||||
|
def new_note(self, ntid: int) -> BackendNote:
|
||||||
|
return self._run_command(pb.BackendInput(new_note=ntid)).new_note
|
||||||
|
|
||||||
|
def add_note(self, note: BackendNote, deck_id: int) -> int:
|
||||||
|
return self._run_command(
|
||||||
|
pb.BackendInput(add_note=pb.AddNoteIn(note=note, deck_id=deck_id))
|
||||||
|
).add_note
|
||||||
|
|
||||||
|
def update_note(self, note: BackendNote) -> None:
|
||||||
|
self._run_command(pb.BackendInput(update_note=note))
|
||||||
|
|
||||||
|
def get_note(self, nid) -> Optional[BackendNote]:
|
||||||
|
try:
|
||||||
|
return self._run_command(pb.BackendInput(get_note=nid)).get_note
|
||||||
|
except NotFoundError:
|
||||||
|
return None
|
||||||
|
|
||||||
def translate_string_in(
|
def translate_string_in(
|
||||||
key: TR, **kwargs: Union[str, int, float]
|
key: TR, **kwargs: Union[str, int, float]
|
||||||
|
|
|
@ -69,6 +69,7 @@ def test_genrem():
|
||||||
mm.save(m, templates=True)
|
mm.save(m, templates=True)
|
||||||
assert len(f.cards()) == 2
|
assert len(f.cards()) == 2
|
||||||
# if the template is changed to remove cards, they'll be removed
|
# if the template is changed to remove cards, they'll be removed
|
||||||
|
t = m["tmpls"][1]
|
||||||
t["qfmt"] = "{{Back}}"
|
t["qfmt"] = "{{Back}}"
|
||||||
mm.save(m, templates=True)
|
mm.save(m, templates=True)
|
||||||
d.remCards(d.emptyCids())
|
d.remCards(d.emptyCids())
|
||||||
|
|
|
@ -60,11 +60,6 @@ def test_noteAddDelete():
|
||||||
t["afmt"] = "{{Front}}"
|
t["afmt"] = "{{Front}}"
|
||||||
mm.addTemplate(m, t)
|
mm.addTemplate(m, t)
|
||||||
mm.save(m)
|
mm.save(m)
|
||||||
# the default save doesn't generate cards
|
|
||||||
assert deck.cardCount() == 1
|
|
||||||
# but when templates are edited such as in the card layout screen, it
|
|
||||||
# should generate cards on close
|
|
||||||
mm.save(m, templates=True, updateReqs=False)
|
|
||||||
assert deck.cardCount() == 2
|
assert deck.cardCount() == 2
|
||||||
# creating new notes should use both cards
|
# creating new notes should use both cards
|
||||||
f = deck.newNote()
|
f = deck.newNote()
|
||||||
|
@ -124,10 +119,10 @@ def test_addDelTags():
|
||||||
|
|
||||||
def test_timestamps():
|
def test_timestamps():
|
||||||
deck = getEmptyCol()
|
deck = getEmptyCol()
|
||||||
assert len(deck.models.models) == len(models)
|
assert len(deck.models.all_names_and_ids()) == len(models)
|
||||||
for i in range(100):
|
for i in range(100):
|
||||||
addBasicModel(deck)
|
addBasicModel(deck)
|
||||||
assert len(deck.models.models) == 100 + len(models)
|
assert len(deck.models.all_names_and_ids()) == 100 + len(models)
|
||||||
|
|
||||||
|
|
||||||
def test_furigana():
|
def test_furigana():
|
||||||
|
|
|
@ -17,8 +17,8 @@ def test_findCards():
|
||||||
f["Front"] = "dog"
|
f["Front"] = "dog"
|
||||||
f["Back"] = "cat"
|
f["Back"] = "cat"
|
||||||
f.tags.append("monkey animal_1 * %")
|
f.tags.append("monkey animal_1 * %")
|
||||||
f1id = f.id
|
|
||||||
deck.addNote(f)
|
deck.addNote(f)
|
||||||
|
f1id = f.id
|
||||||
firstCardId = f.cards()[0].id
|
firstCardId = f.cards()[0].id
|
||||||
f = deck.newNote()
|
f = deck.newNote()
|
||||||
f["Front"] = "goats are fun"
|
f["Front"] = "goats are fun"
|
||||||
|
@ -32,6 +32,7 @@ def test_findCards():
|
||||||
deck.addNote(f)
|
deck.addNote(f)
|
||||||
catCard = f.cards()[0]
|
catCard = f.cards()[0]
|
||||||
m = deck.models.current()
|
m = deck.models.current()
|
||||||
|
m = deck.models.copy(m)
|
||||||
mm = deck.models
|
mm = deck.models
|
||||||
t = mm.newTemplate("Reverse")
|
t = mm.newTemplate("Reverse")
|
||||||
t["qfmt"] = "{{Back}}"
|
t["qfmt"] = "{{Back}}"
|
||||||
|
@ -130,8 +131,8 @@ def test_findCards():
|
||||||
!= firstCardId
|
!= firstCardId
|
||||||
)
|
)
|
||||||
# model
|
# model
|
||||||
assert len(deck.findCards("note:basic")) == 5
|
assert len(deck.findCards("note:basic")) == 3
|
||||||
assert len(deck.findCards("-note:basic")) == 0
|
assert len(deck.findCards("-note:basic")) == 2
|
||||||
assert len(deck.findCards("-note:foo")) == 5
|
assert len(deck.findCards("-note:foo")) == 5
|
||||||
# deck
|
# deck
|
||||||
assert len(deck.findCards("deck:default")) == 5
|
assert len(deck.findCards("deck:default")) == 5
|
||||||
|
|
|
@ -26,7 +26,7 @@ def test_add():
|
||||||
def test_strings():
|
def test_strings():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
mf = d.media.filesInStr
|
mf = d.media.filesInStr
|
||||||
mid = list(d.models.models.keys())[0]
|
mid = d.models.current()["id"]
|
||||||
assert mf(mid, "aoeu") == []
|
assert mf(mid, "aoeu") == []
|
||||||
assert mf(mid, "aoeu<img src='foo.jpg'>ao") == ["foo.jpg"]
|
assert mf(mid, "aoeu<img src='foo.jpg'>ao") == ["foo.jpg"]
|
||||||
assert mf(mid, "aoeu<img src='foo.jpg' style='test'>ao") == ["foo.jpg"]
|
assert mf(mid, "aoeu<img src='foo.jpg' style='test'>ao") == ["foo.jpg"]
|
||||||
|
|
|
@ -48,6 +48,7 @@ def test_fields():
|
||||||
assert d.getNote(d.models.nids(m)[0]).fields == ["1", "2", ""]
|
assert d.getNote(d.models.nids(m)[0]).fields == ["1", "2", ""]
|
||||||
assert d.models.scmhash(m) != h
|
assert d.models.scmhash(m) != h
|
||||||
# rename it
|
# rename it
|
||||||
|
f = m["flds"][2]
|
||||||
d.models.renameField(m, f, "bar")
|
d.models.renameField(m, f, "bar")
|
||||||
assert d.getNote(d.models.nids(m)[0])["bar"] == ""
|
assert d.getNote(d.models.nids(m)[0])["bar"] == ""
|
||||||
# delete back
|
# delete back
|
||||||
|
@ -102,7 +103,7 @@ def test_templates():
|
||||||
assert c.ord == 1
|
assert c.ord == 1
|
||||||
assert c2.ord == 0
|
assert c2.ord == 0
|
||||||
# removing a template should delete its cards
|
# removing a template should delete its cards
|
||||||
assert d.models.remTemplate(m, m["tmpls"][0])
|
d.models.remTemplate(m, m["tmpls"][0])
|
||||||
assert d.cardCount() == 1
|
assert d.cardCount() == 1
|
||||||
# and should have updated the other cards' ordinals
|
# and should have updated the other cards' ordinals
|
||||||
c = f.cards()[0]
|
c = f.cards()[0]
|
||||||
|
@ -111,7 +112,11 @@ def test_templates():
|
||||||
# it shouldn't be possible to orphan notes by removing templates
|
# it shouldn't be possible to orphan notes by removing templates
|
||||||
t = mm.newTemplate("template name")
|
t = mm.newTemplate("template name")
|
||||||
mm.addTemplate(m, t)
|
mm.addTemplate(m, t)
|
||||||
assert not d.models.remTemplate(m, m["tmpls"][0])
|
d.models.remTemplate(m, m["tmpls"][0])
|
||||||
|
assert (
|
||||||
|
d.db.scalar("select count() from cards where nid not in (select id from notes)")
|
||||||
|
== 0
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_cloze_ordinals():
|
def test_cloze_ordinals():
|
||||||
|
@ -269,7 +274,6 @@ def test_chained_mods():
|
||||||
|
|
||||||
def test_modelChange():
|
def test_modelChange():
|
||||||
deck = getEmptyCol()
|
deck = getEmptyCol()
|
||||||
basic = deck.models.byName("Basic")
|
|
||||||
cloze = deck.models.byName("Cloze")
|
cloze = deck.models.byName("Cloze")
|
||||||
# enable second template and add a note
|
# enable second template and add a note
|
||||||
m = deck.models.current()
|
m = deck.models.current()
|
||||||
|
@ -279,6 +283,7 @@ def test_modelChange():
|
||||||
t["afmt"] = "{{Front}}"
|
t["afmt"] = "{{Front}}"
|
||||||
mm.addTemplate(m, t)
|
mm.addTemplate(m, t)
|
||||||
mm.save(m)
|
mm.save(m)
|
||||||
|
basic = m
|
||||||
f = deck.newNote()
|
f = deck.newNote()
|
||||||
f["Front"] = "f"
|
f["Front"] = "f"
|
||||||
f["Back"] = "b123"
|
f["Back"] = "b123"
|
||||||
|
@ -334,8 +339,9 @@ def test_modelChange():
|
||||||
f["Front"] = "f2"
|
f["Front"] = "f2"
|
||||||
f["Back"] = "b2"
|
f["Back"] = "b2"
|
||||||
deck.addNote(f)
|
deck.addNote(f)
|
||||||
assert deck.models.useCount(basic) == 2
|
counts = deck.models.all_use_counts()
|
||||||
assert deck.models.useCount(cloze) == 0
|
assert next(c.use_count for c in counts if c.name == "Basic") == 2
|
||||||
|
assert next(c.use_count for c in counts if c.name == "Cloze") == 0
|
||||||
map = {0: 0, 1: 1}
|
map = {0: 0, 1: 1}
|
||||||
deck.models.change(basic, [f.id], cloze, map, map)
|
deck.models.change(basic, [f.id], cloze, map, map)
|
||||||
f.load()
|
f.load()
|
||||||
|
@ -362,13 +368,16 @@ def test_availOrds():
|
||||||
mm.save(m, templates=True)
|
mm.save(m, templates=True)
|
||||||
assert not mm.availOrds(m, joinFields(f.fields))
|
assert not mm.availOrds(m, joinFields(f.fields))
|
||||||
# AND
|
# AND
|
||||||
|
t = m["tmpls"][0]
|
||||||
t["qfmt"] = "{{#Front}}{{#Back}}{{Front}}{{/Back}}{{/Front}}"
|
t["qfmt"] = "{{#Front}}{{#Back}}{{Front}}{{/Back}}{{/Front}}"
|
||||||
mm.save(m, templates=True)
|
mm.save(m, templates=True)
|
||||||
assert not mm.availOrds(m, joinFields(f.fields))
|
assert not mm.availOrds(m, joinFields(f.fields))
|
||||||
|
t = m["tmpls"][0]
|
||||||
t["qfmt"] = "{{#Front}}\n{{#Back}}\n{{Front}}\n{{/Back}}\n{{/Front}}"
|
t["qfmt"] = "{{#Front}}\n{{#Back}}\n{{Front}}\n{{/Back}}\n{{/Front}}"
|
||||||
mm.save(m, templates=True)
|
mm.save(m, templates=True)
|
||||||
assert not mm.availOrds(m, joinFields(f.fields))
|
assert not mm.availOrds(m, joinFields(f.fields))
|
||||||
# OR
|
# OR
|
||||||
|
t = m["tmpls"][0]
|
||||||
t["qfmt"] = "{{Front}}\n{{Back}}"
|
t["qfmt"] = "{{Front}}\n{{Back}}"
|
||||||
mm.save(m, templates=True)
|
mm.save(m, templates=True)
|
||||||
assert mm.availOrds(m, joinFields(f.fields)) == [0]
|
assert mm.availOrds(m, joinFields(f.fields)) == [0]
|
||||||
|
|
|
@ -1176,11 +1176,11 @@ QTableView {{ gridline-color: {grid} }}
|
||||||
|
|
||||||
def _modelTree(self, root) -> None:
|
def _modelTree(self, root) -> None:
|
||||||
assert self.col
|
assert self.col
|
||||||
for m in sorted(self.col.models.all(), key=itemgetter("name")):
|
for m in self.col.models.all_names_and_ids():
|
||||||
item = SidebarItem(
|
item = SidebarItem(
|
||||||
m["name"],
|
m.name,
|
||||||
":/icons/notetype.svg",
|
":/icons/notetype.svg",
|
||||||
lambda m=m: self.setFilter("note", m["name"]), # type: ignore
|
lambda m=m: self.setFilter("note", m.name), # type: ignore
|
||||||
)
|
)
|
||||||
root.addChild(item)
|
root.addChild(item)
|
||||||
|
|
||||||
|
|
|
@ -239,21 +239,12 @@ class CardLayout(QDialog):
|
||||||
if len(self.model["tmpls"]) < 2:
|
if len(self.model["tmpls"]) < 2:
|
||||||
return showInfo(_("At least one card type is required."))
|
return showInfo(_("At least one card type is required."))
|
||||||
idx = self.ord
|
idx = self.ord
|
||||||
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(
|
msg = _("Delete the '%(a)s' card type, and its %(b)s?") % dict(
|
||||||
a=self.model["tmpls"][idx]["name"], b=cards
|
a=self.model["tmpls"][idx]["name"], b=_("cards")
|
||||||
)
|
)
|
||||||
if not askUser(msg):
|
if not askUser(msg):
|
||||||
return
|
return
|
||||||
if not self.mm.remTemplate(self.model, self.cards[idx].template()):
|
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."""
|
|
||||||
)
|
|
||||||
)
|
|
||||||
self.redraw()
|
self.redraw()
|
||||||
|
|
||||||
def removeColons(self):
|
def removeColons(self):
|
||||||
|
|
|
@ -4,10 +4,12 @@ import collections
|
||||||
import re
|
import re
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from typing import List
|
||||||
|
|
||||||
import aqt.clayout
|
import aqt.clayout
|
||||||
from anki import stdmodels
|
from anki import stdmodels
|
||||||
from anki.lang import _, ngettext
|
from anki.lang import _, ngettext
|
||||||
|
from anki.rsbackend import pb
|
||||||
from aqt import AnkiQt, gui_hooks
|
from aqt import AnkiQt, gui_hooks
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
from aqt.utils import (
|
from aqt.utils import (
|
||||||
|
@ -34,6 +36,7 @@ class Models(QDialog):
|
||||||
self.form = aqt.forms.models.Ui_Dialog()
|
self.form = aqt.forms.models.Ui_Dialog()
|
||||||
self.form.setupUi(self)
|
self.form.setupUi(self)
|
||||||
qconnect(self.form.buttonBox.helpRequested, lambda: openHelp("notetypes"))
|
qconnect(self.form.buttonBox.helpRequested, lambda: openHelp("notetypes"))
|
||||||
|
self.models: List[pb.NoteTypeNameIDUseCount] = []
|
||||||
self.setupModels()
|
self.setupModels()
|
||||||
restoreGeom(self, "models")
|
restoreGeom(self, "models")
|
||||||
self.exec_()
|
self.exec_()
|
||||||
|
@ -76,13 +79,12 @@ class Models(QDialog):
|
||||||
row = self.form.modelsList.currentRow()
|
row = self.form.modelsList.currentRow()
|
||||||
if row == -1:
|
if row == -1:
|
||||||
row = 0
|
row = 0
|
||||||
self.models = self.col.models.all()
|
self.models = self.col.models.all_use_counts()
|
||||||
self.models.sort(key=itemgetter("name"))
|
|
||||||
self.form.modelsList.clear()
|
self.form.modelsList.clear()
|
||||||
for m in self.models:
|
for m in self.models:
|
||||||
mUse = self.mm.useCount(m)
|
mUse = m.use_count
|
||||||
mUse = ngettext("%d note", "%d notes", mUse) % mUse
|
mUse = ngettext("%d note", "%d notes", mUse) % mUse
|
||||||
item = QListWidgetItem("%s [%s]" % (m["name"], mUse))
|
item = QListWidgetItem("%s [%s]" % (m.name, mUse))
|
||||||
self.form.modelsList.addItem(item)
|
self.form.modelsList.addItem(item)
|
||||||
self.form.modelsList.setCurrentRow(row)
|
self.form.modelsList.setCurrentRow(row)
|
||||||
|
|
||||||
|
@ -90,7 +92,7 @@ class Models(QDialog):
|
||||||
if self.model:
|
if self.model:
|
||||||
self.saveModel()
|
self.saveModel()
|
||||||
idx = self.form.modelsList.currentRow()
|
idx = self.form.modelsList.currentRow()
|
||||||
self.model = self.models[idx]
|
self.model = self.col.models.get(self.models[idx].id)
|
||||||
|
|
||||||
def onAdd(self):
|
def onAdd(self):
|
||||||
m = AddModel(self.mw, self).get()
|
m = AddModel(self.mw, self).get()
|
||||||
|
|
Loading…
Reference in a new issue