mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 06:22: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.changed = False
|
||||
self.models.models = self.backend.get_all_notetypes()
|
||||
self.models.changed = False
|
||||
|
||||
def setMod(self) -> None:
|
||||
"""Mark DB modified.
|
||||
|
@ -232,6 +230,7 @@ crt=?, mod=?, scm=?, dty=?, usn=?, ls=?""",
|
|||
self.save(trx=False)
|
||||
else:
|
||||
self.db.rollback()
|
||||
self.models._clear_cache()
|
||||
self.backend.close_collection(downgrade=downgrade)
|
||||
self.db = None
|
||||
self.media.close()
|
||||
|
@ -319,21 +318,12 @@ crt=?, mod=?, scm=?, dty=?, usn=?, ls=?""",
|
|||
"Return a new note with the current model."
|
||||
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:
|
||||
"""Add a note to the collection. Return number of new cards."""
|
||||
# check we have card models available, then save
|
||||
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
|
||||
self.add_note(note, note.model()["did"])
|
||||
return len(note.cards())
|
||||
|
||||
def remNotes(self, ids: Iterable[int]) -> None:
|
||||
"""Deletes notes with the given IDs."""
|
||||
|
|
|
@ -226,7 +226,7 @@ class AnkiExporter(Exporter):
|
|||
# need to reset card state
|
||||
self.dst.sched.resetCards(cids)
|
||||
# models - start with zero
|
||||
self.dst.models.models = {}
|
||||
self.dst.models.remove_all_notetypes()
|
||||
for m in self.src.models.all():
|
||||
if int(m["id"]) in mids:
|
||||
self.dst.models.update(m)
|
||||
|
|
|
@ -10,7 +10,7 @@ import time
|
|||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from typing import Any, Callable, List, Optional, Tuple, Union
|
||||
from typing import Any, Callable, List, Optional, Tuple
|
||||
|
||||
import anki
|
||||
from anki.consts import *
|
||||
|
@ -122,7 +122,7 @@ class MediaManager:
|
|||
##########################################################################
|
||||
|
||||
def filesInStr(
|
||||
self, mid: Union[int, str], string: str, includeRemote: bool = False
|
||||
self, mid: int, string: str, includeRemote: bool = False
|
||||
) -> List[str]:
|
||||
l = []
|
||||
model = self.col.models.get(mid)
|
||||
|
|
|
@ -6,9 +6,10 @@ from __future__ import annotations
|
|||
import copy
|
||||
import re
|
||||
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.backend_pb2 as pb
|
||||
from anki import hooks
|
||||
from anki.consts import *
|
||||
from anki.lang import _
|
||||
|
@ -23,12 +24,16 @@ TemplateRequirementType = str # Union["all", "any", "none"]
|
|||
TemplateRequiredFieldOrds = Tuple[int, TemplateRequirementType, List[int]]
|
||||
AllTemplateReqs = List[TemplateRequiredFieldOrds]
|
||||
|
||||
# fixme: memory leaks
|
||||
# fixme: syncing, beforeUpload
|
||||
|
||||
# Models
|
||||
##########################################################################
|
||||
|
||||
# - careful not to add any lists/dicts/etc here, as they aren't deep copied
|
||||
|
||||
defaultModel: NoteType = {
|
||||
"id": 0,
|
||||
"sortf": 0,
|
||||
"did": 1,
|
||||
"latexPre": """\
|
||||
|
@ -43,7 +48,7 @@ defaultModel: NoteType = {
|
|||
"latexPost": "\\end{document}",
|
||||
"mod": 0,
|
||||
"usn": 0,
|
||||
"vers": [], # FIXME: remove when other clients have caught up
|
||||
"req": [],
|
||||
"type": MODEL_STD,
|
||||
"css": """\
|
||||
.card {
|
||||
|
@ -83,50 +88,131 @@ defaultTemplate: Template = {
|
|||
}
|
||||
|
||||
|
||||
class ModelManager:
|
||||
models: Dict[str, NoteType]
|
||||
class ModelsDictProxy:
|
||||
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
|
||||
#############################################################
|
||||
|
||||
def __init__(self, col: anki.storage._Collection) -> None:
|
||||
self.col = col.weakref()
|
||||
self.models = {}
|
||||
self.changed = False
|
||||
self.models = ModelsDictProxy(col)
|
||||
# do not access this directly!
|
||||
self._cache = {}
|
||||
|
||||
def save(
|
||||
self,
|
||||
m: Optional[NoteType] = None,
|
||||
m: NoteType = None,
|
||||
# no longer used
|
||||
templates: bool = False,
|
||||
updateReqs: bool = True,
|
||||
) -> None:
|
||||
"Mark M modified if provided, and schedule registry flush."
|
||||
if m and m["id"]:
|
||||
m["mod"] = intTime()
|
||||
m["usn"] = self.col.usn()
|
||||
if updateReqs:
|
||||
self._updateRequired(m)
|
||||
if templates:
|
||||
self._syncTemplates(m)
|
||||
self.changed = True
|
||||
"Save changes made to provided note type."
|
||||
if not m:
|
||||
print("col.models.save() should be passed the changed notetype")
|
||||
return
|
||||
|
||||
self.update(m, preserve_usn=False)
|
||||
|
||||
# fixme: badly named; also fires on updates
|
||||
hooks.note_type_added(m)
|
||||
|
||||
# legacy
|
||||
def flush(self) -> None:
|
||||
"Flush the registry if any models were changed."
|
||||
if self.changed:
|
||||
self.ensureNotEmpty()
|
||||
self.col.backend.set_all_notetypes(self.models)
|
||||
self.changed = False
|
||||
pass
|
||||
|
||||
# fixme: enforce at lower level
|
||||
def ensureNotEmpty(self) -> Optional[bool]:
|
||||
if not self.models:
|
||||
if not self.all_names_and_ids():
|
||||
from anki.stdmodels import addBasicModel
|
||||
|
||||
addBasicModel(self.col)
|
||||
return True
|
||||
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:
|
||||
|
@ -134,33 +220,46 @@ class ModelManager:
|
|||
m = self.get(self.col.decks.current().get("mid"))
|
||||
if not forDeck or not m:
|
||||
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:
|
||||
self.col.conf["curModel"] = m["id"]
|
||||
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."
|
||||
id = str(id)
|
||||
if id in self.models:
|
||||
return self.models[id]
|
||||
# deal with various legacy input types
|
||||
if id is None:
|
||||
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."
|
||||
return list(self.models.values())
|
||||
return [self.get(nt.id) for nt in self.all_names_and_ids()]
|
||||
|
||||
def allNames(self) -> List:
|
||||
return [m["name"] for m in self.all()]
|
||||
|
||||
def byName(self, name: str) -> Any:
|
||||
def byName(self, name: str) -> Optional[NoteType]:
|
||||
"Get model with NAME."
|
||||
for m in list(self.models.values()):
|
||||
if m["name"] == name:
|
||||
return m
|
||||
id = self.id_for_name(name)
|
||||
if id:
|
||||
return self.get(id)
|
||||
else:
|
||||
return None
|
||||
|
||||
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
|
||||
m = defaultModel.copy()
|
||||
m["name"] = name
|
||||
|
@ -168,59 +267,50 @@ class ModelManager:
|
|||
m["flds"] = []
|
||||
m["tmpls"] = []
|
||||
m["tags"] = []
|
||||
m["id"] = None
|
||||
m["id"] = 0
|
||||
return m
|
||||
|
||||
def rem(self, m: NoteType) -> None:
|
||||
"Delete model, and all its cards/notes."
|
||||
self.remove(m["id"])
|
||||
|
||||
def remove_all_notetypes(self):
|
||||
self.col.modSchema(check=True)
|
||||
current = self.current()["id"] == m["id"]
|
||||
# delete notes/cards
|
||||
self.col.remCards(
|
||||
self.col.db.list(
|
||||
"""
|
||||
select id from cards where nid in (select id from notes where mid = ?)""",
|
||||
m["id"],
|
||||
)
|
||||
)
|
||||
# then the model
|
||||
del self.models[str(m["id"])]
|
||||
self.save()
|
||||
# GUI should ensure last model is not deleted
|
||||
if current:
|
||||
self.setCurrent(list(self.models.values())[0])
|
||||
for nt in self.all_names_and_ids():
|
||||
self._remove_from_cache(nt.id)
|
||||
self.col.backend.remove_notetype(nt.id)
|
||||
|
||||
def remove(self, id: int) -> None:
|
||||
self.col.modSchema(check=True)
|
||||
self._remove_from_cache(id)
|
||||
was_current = self.current()["id"] == id
|
||||
self.col.backend.remove_notetype(id)
|
||||
|
||||
# fixme: handle in backend
|
||||
if was_current:
|
||||
self.col.conf["curModel"] = self.all_names_and_ids()[0].id
|
||||
|
||||
def add(self, m: NoteType) -> None:
|
||||
self._setID(m)
|
||||
self.update(m)
|
||||
self.setCurrent(m)
|
||||
self.save(m)
|
||||
|
||||
def ensureNameUnique(self, m: NoteType) -> None:
|
||||
for mcur in self.all():
|
||||
if mcur["name"] == m["name"] and mcur["id"] != m["id"]:
|
||||
m["name"] += "-" + checksum(str(time.time()))[:5]
|
||||
break
|
||||
existing_id = self.id_for_name(m["name"])
|
||||
if existing_id is not None and existing_id != m["id"]:
|
||||
m["name"] += "-" + checksum(str(time.time()))[:5]
|
||||
|
||||
def update(self, m: NoteType) -> None:
|
||||
"Add or update an existing model. Used for syncing and merging."
|
||||
def update(self, m: NoteType, preserve_usn=True) -> None:
|
||||
"Add or update an existing model. Use .save() instead."
|
||||
self._remove_from_cache(m["id"])
|
||||
self.ensureNameUnique(m)
|
||||
self.models[str(m["id"])] = m
|
||||
# mark registry changed, but don't bump mod time
|
||||
self.save()
|
||||
self.col.backend.add_or_update_notetype(m, preserve_usn=preserve_usn)
|
||||
self.setCurrent(m)
|
||||
self._mutate_after_write(m)
|
||||
|
||||
def _setID(self, m: NoteType) -> None:
|
||||
while 1:
|
||||
id = str(intTime(1000))
|
||||
if id not in self.models:
|
||||
break
|
||||
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())
|
||||
def _mutate_after_write(self, nt: NoteType) -> None:
|
||||
# existing code expects the note type to be mutated to reflect
|
||||
# the changes made when adding, such as ordinal assignment :-(
|
||||
updated = self.get(nt["id"])
|
||||
nt.update(updated)
|
||||
|
||||
# Tools
|
||||
##################################################
|
||||
|
@ -231,17 +321,9 @@ select id from cards where nid in (select id from notes where mid = ?)""",
|
|||
|
||||
def useCount(self, m: NoteType) -> Any:
|
||||
"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"])
|
||||
|
||||
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
|
||||
##################################################
|
||||
|
||||
|
@ -249,18 +331,13 @@ and notes.mid = ? and cards.ord = ?""",
|
|||
"Copy, save and return."
|
||||
m2 = copy.deepcopy(m)
|
||||
m2["name"] = _("%s copy") % m2["name"]
|
||||
m2["id"] = 0
|
||||
self.add(m2)
|
||||
return m2
|
||||
|
||||
# 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]]:
|
||||
"Mapping of field name -> (ord, field)."
|
||||
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:
|
||||
assert 0 <= idx < len(m["flds"])
|
||||
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:
|
||||
# only mod schema if model isn't new
|
||||
if m["id"]:
|
||||
self.col.modSchema(check=True)
|
||||
m["flds"].append(field)
|
||||
self._updateFieldOrds(m)
|
||||
m["sortf"] = idx
|
||||
|
||||
self.save(m)
|
||||
|
||||
def add(fields):
|
||||
fields.append("")
|
||||
return fields
|
||||
# Adding & changing 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:
|
||||
self.col.modSchema(check=True)
|
||||
# save old sort field
|
||||
sortFldName = m["flds"][m["sortf"]]["name"]
|
||||
idx = m["flds"].index(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):
|
||||
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)
|
||||
self.save(m)
|
||||
|
||||
def moveField(self, m: NoteType, field: Field, idx: int) -> None:
|
||||
self.col.modSchema(check=True)
|
||||
oldidx = m["flds"].index(field)
|
||||
if oldidx == idx:
|
||||
return
|
||||
# remember old sort field
|
||||
sortf = m["flds"][m["sortf"]]
|
||||
# move
|
||||
|
||||
m["flds"].remove(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)
|
||||
|
||||
def _updateFieldOrds(self, m: NoteType) -> None:
|
||||
for c, f in enumerate(m["flds"]):
|
||||
f["ord"] = c
|
||||
def renameField(self, m: NoteType, field: Field, newName: str) -> None:
|
||||
assert field in m["flds"]
|
||||
|
||||
def _transformFields(self, m: NoteType, fn: Callable) -> None:
|
||||
# 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)
|
||||
field["name"] = newName
|
||||
|
||||
# Templates
|
||||
self.save(m)
|
||||
|
||||
# Adding & changing templates
|
||||
##################################################
|
||||
|
||||
def newTemplate(self, name: str) -> Template:
|
||||
|
@ -387,84 +408,33 @@ and notes.mid = ? and cards.ord = ?""",
|
|||
return t
|
||||
|
||||
def addTemplate(self, m: NoteType, template: Template) -> None:
|
||||
"Note: should col.genCards() afterwards."
|
||||
if m["id"]:
|
||||
self.col.modSchema(check=True)
|
||||
|
||||
m["tmpls"].append(template)
|
||||
self._updateTemplOrds(m)
|
||||
self.save(m)
|
||||
|
||||
def remTemplate(self, m: NoteType, template: Template) -> bool:
|
||||
"False if removing template would leave orphan notes."
|
||||
if m["id"]:
|
||||
self.save(m)
|
||||
|
||||
def remTemplate(self, m: NoteType, template: Template) -> None:
|
||||
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.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:
|
||||
for c, t in enumerate(m["tmpls"]):
|
||||
t["ord"] = c
|
||||
m["tmpls"].remove(template)
|
||||
|
||||
self.save(m)
|
||||
|
||||
def moveTemplate(self, m: NoteType, template: Template, idx: int) -> None:
|
||||
self.col.modSchema(check=True)
|
||||
|
||||
oldidx = m["tmpls"].index(template)
|
||||
if oldidx == idx:
|
||||
return
|
||||
oldidxs = dict((id(t), t["ord"]) for t in m["tmpls"])
|
||||
|
||||
m["tmpls"].remove(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:
|
||||
rem = self.col.genCards(self.nids(m))
|
||||
self.save(m)
|
||||
|
||||
# Model changing
|
||||
##########################################################################
|
||||
|
|
|
@ -3,35 +3,19 @@
|
|||
|
||||
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
|
||||
from anki import hooks
|
||||
from anki.models import Field, NoteType
|
||||
from anki.utils import (
|
||||
fieldChecksum,
|
||||
guid64,
|
||||
intTime,
|
||||
joinFields,
|
||||
splitFields,
|
||||
stripHTMLMedia,
|
||||
timestampID,
|
||||
)
|
||||
from anki.models import NoteType
|
||||
from anki.rsbackend import BackendNote
|
||||
from anki.utils import fieldChecksum, joinFields, splitFields, stripHTMLMedia
|
||||
|
||||
|
||||
class Note:
|
||||
col: anki.storage._Collection
|
||||
newlyAdded: bool
|
||||
id: int
|
||||
guid: str
|
||||
_model: NoteType
|
||||
mid: int
|
||||
tags: List[str]
|
||||
fields: List[str]
|
||||
flags: int
|
||||
data: str
|
||||
_fmap: Dict[str, Tuple[int, Field]]
|
||||
scm: int
|
||||
# not currently exposed
|
||||
flags = 0
|
||||
data = ""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
@ -41,78 +25,51 @@ class Note:
|
|||
) -> None:
|
||||
assert not (model and id)
|
||||
self.col = col.weakref()
|
||||
self.newlyAdded = False
|
||||
# self.newlyAdded = False
|
||||
|
||||
if id:
|
||||
# existing note
|
||||
self.id = id
|
||||
self.load()
|
||||
else:
|
||||
self.id = timestampID(col.db, "notes")
|
||||
self.guid = guid64()
|
||||
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
|
||||
# new note for provided notetype
|
||||
self._load_from_backend_note(self.col.backend.new_note(model["id"]))
|
||||
|
||||
def load(self) -> None:
|
||||
(
|
||||
self.guid,
|
||||
self.mid,
|
||||
self.mod,
|
||||
self.usn,
|
||||
tags,
|
||||
fields,
|
||||
self.flags,
|
||||
self.data,
|
||||
) = self.col.db.first(
|
||||
"""
|
||||
select guid, mid, mod, usn, tags, flds, flags, data
|
||||
from notes where id = ?""",
|
||||
self.id,
|
||||
)
|
||||
self.fields = splitFields(fields)
|
||||
self.tags = self.col.tags.split(tags)
|
||||
n = self.col.backend.get_note(self.id)
|
||||
assert n
|
||||
self._load_from_backend_note(n)
|
||||
|
||||
def _load_from_backend_note(self, n: BackendNote) -> None:
|
||||
self.id = n.id
|
||||
self.guid = n.guid
|
||||
self.mid = n.ntid
|
||||
self.mod = n.mtime_secs
|
||||
self.usn = n.usn
|
||||
self.tags = list(n.tags)
|
||||
self.fields = list(n.fields)
|
||||
|
||||
self._model = self.col.models.get(self.mid)
|
||||
self._fmap = self.col.models.fieldMap(self._model)
|
||||
self.scm = self.col.scm
|
||||
|
||||
def flush(self, mod: Optional[int] = None) -> None:
|
||||
"If fields or tags have changed, write changes to disk."
|
||||
assert self.scm == self.col.scm
|
||||
self._preFlush()
|
||||
sfld = stripHTMLMedia(self.fields[self.col.models.sortIdx(self._model)])
|
||||
tags = self.stringTags()
|
||||
fields = self.joinedFields()
|
||||
if not mod and self.col.db.scalar(
|
||||
"select 1 from notes where id = ? and tags = ? and flds = ?",
|
||||
self.id,
|
||||
tags,
|
||||
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,
|
||||
# fixme: only save tags in list on save
|
||||
def to_backend_note(self) -> BackendNote:
|
||||
hooks.note_will_flush(self)
|
||||
return BackendNote(
|
||||
id=self.id,
|
||||
guid=self.guid,
|
||||
ntid=self.mid,
|
||||
mtime_secs=self.mod,
|
||||
usn=self.usn,
|
||||
# fixme: catch spaces in individual tags
|
||||
tags=" ".join(self.tags).split(" "),
|
||||
fields=self.fields,
|
||||
)
|
||||
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:
|
||||
return joinFields(self.fields)
|
||||
|
@ -198,22 +155,3 @@ insert or replace into notes values (?,?,?,?,?,?,?,?,?,?,?)""",
|
|||
if stripHTMLMedia(splitFields(flds)[0]) == stripHTMLMedia(self.fields[0]):
|
||||
return 2
|
||||
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
|
||||
BuiltinSortKind = pb.BuiltinSearchOrder.BuiltinSortKind
|
||||
BackendCard = pb.Card
|
||||
BackendNote = pb.Note
|
||||
TagUsnTuple = pb.TagUsnTuple
|
||||
NoteType = pb.NoteType
|
||||
|
||||
|
@ -98,6 +99,10 @@ class TemplateError(StringError):
|
|||
pass
|
||||
|
||||
|
||||
class NotFoundError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def proto_exception_to_native(err: pb.BackendError) -> Exception:
|
||||
val = err.WhichOneof("value")
|
||||
if val == "interrupted":
|
||||
|
@ -116,6 +121,8 @@ def proto_exception_to_native(err: pb.BackendError) -> Exception:
|
|||
return StringError(err.localized)
|
||||
elif val == "json_error":
|
||||
return StringError(err.localized)
|
||||
elif val == "not_found_error":
|
||||
return NotFoundError()
|
||||
else:
|
||||
assert_impossible_literal(val)
|
||||
|
||||
|
@ -609,15 +616,12 @@ class RustBackend:
|
|||
def set_all_config(self, conf: Dict[str, Any]):
|
||||
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(
|
||||
pb.BackendInput(get_all_notetypes=pb.Empty())
|
||||
).get_all_notetypes
|
||||
pb.BackendInput(get_changed_notetypes=usn)
|
||||
).get_changed_notetypes
|
||||
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]]:
|
||||
jstr = self._run_command(
|
||||
pb.BackendInput(get_all_decks=pb.Empty())
|
||||
|
@ -634,6 +638,67 @@ class RustBackend:
|
|||
).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(
|
||||
key: TR, **kwargs: Union[str, int, float]
|
||||
|
|
|
@ -69,6 +69,7 @@ def test_genrem():
|
|||
mm.save(m, templates=True)
|
||||
assert len(f.cards()) == 2
|
||||
# if the template is changed to remove cards, they'll be removed
|
||||
t = m["tmpls"][1]
|
||||
t["qfmt"] = "{{Back}}"
|
||||
mm.save(m, templates=True)
|
||||
d.remCards(d.emptyCids())
|
||||
|
|
|
@ -60,11 +60,6 @@ def test_noteAddDelete():
|
|||
t["afmt"] = "{{Front}}"
|
||||
mm.addTemplate(m, t)
|
||||
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
|
||||
# creating new notes should use both cards
|
||||
f = deck.newNote()
|
||||
|
@ -124,10 +119,10 @@ def test_addDelTags():
|
|||
|
||||
def test_timestamps():
|
||||
deck = getEmptyCol()
|
||||
assert len(deck.models.models) == len(models)
|
||||
assert len(deck.models.all_names_and_ids()) == len(models)
|
||||
for i in range(100):
|
||||
addBasicModel(deck)
|
||||
assert len(deck.models.models) == 100 + len(models)
|
||||
assert len(deck.models.all_names_and_ids()) == 100 + len(models)
|
||||
|
||||
|
||||
def test_furigana():
|
||||
|
|
|
@ -17,8 +17,8 @@ def test_findCards():
|
|||
f["Front"] = "dog"
|
||||
f["Back"] = "cat"
|
||||
f.tags.append("monkey animal_1 * %")
|
||||
f1id = f.id
|
||||
deck.addNote(f)
|
||||
f1id = f.id
|
||||
firstCardId = f.cards()[0].id
|
||||
f = deck.newNote()
|
||||
f["Front"] = "goats are fun"
|
||||
|
@ -32,6 +32,7 @@ def test_findCards():
|
|||
deck.addNote(f)
|
||||
catCard = f.cards()[0]
|
||||
m = deck.models.current()
|
||||
m = deck.models.copy(m)
|
||||
mm = deck.models
|
||||
t = mm.newTemplate("Reverse")
|
||||
t["qfmt"] = "{{Back}}"
|
||||
|
@ -130,8 +131,8 @@ def test_findCards():
|
|||
!= firstCardId
|
||||
)
|
||||
# model
|
||||
assert len(deck.findCards("note:basic")) == 5
|
||||
assert len(deck.findCards("-note:basic")) == 0
|
||||
assert len(deck.findCards("note:basic")) == 3
|
||||
assert len(deck.findCards("-note:basic")) == 2
|
||||
assert len(deck.findCards("-note:foo")) == 5
|
||||
# deck
|
||||
assert len(deck.findCards("deck:default")) == 5
|
||||
|
|
|
@ -26,7 +26,7 @@ def test_add():
|
|||
def test_strings():
|
||||
d = getEmptyCol()
|
||||
mf = d.media.filesInStr
|
||||
mid = list(d.models.models.keys())[0]
|
||||
mid = d.models.current()["id"]
|
||||
assert mf(mid, "aoeu") == []
|
||||
assert mf(mid, "aoeu<img src='foo.jpg'>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.models.scmhash(m) != h
|
||||
# rename it
|
||||
f = m["flds"][2]
|
||||
d.models.renameField(m, f, "bar")
|
||||
assert d.getNote(d.models.nids(m)[0])["bar"] == ""
|
||||
# delete back
|
||||
|
@ -102,7 +103,7 @@ def test_templates():
|
|||
assert c.ord == 1
|
||||
assert c2.ord == 0
|
||||
# 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
|
||||
# and should have updated the other cards' ordinals
|
||||
c = f.cards()[0]
|
||||
|
@ -111,7 +112,11 @@ def test_templates():
|
|||
# it shouldn't be possible to orphan notes by removing templates
|
||||
t = mm.newTemplate("template name")
|
||||
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():
|
||||
|
@ -269,7 +274,6 @@ def test_chained_mods():
|
|||
|
||||
def test_modelChange():
|
||||
deck = getEmptyCol()
|
||||
basic = deck.models.byName("Basic")
|
||||
cloze = deck.models.byName("Cloze")
|
||||
# enable second template and add a note
|
||||
m = deck.models.current()
|
||||
|
@ -279,6 +283,7 @@ def test_modelChange():
|
|||
t["afmt"] = "{{Front}}"
|
||||
mm.addTemplate(m, t)
|
||||
mm.save(m)
|
||||
basic = m
|
||||
f = deck.newNote()
|
||||
f["Front"] = "f"
|
||||
f["Back"] = "b123"
|
||||
|
@ -334,8 +339,9 @@ def test_modelChange():
|
|||
f["Front"] = "f2"
|
||||
f["Back"] = "b2"
|
||||
deck.addNote(f)
|
||||
assert deck.models.useCount(basic) == 2
|
||||
assert deck.models.useCount(cloze) == 0
|
||||
counts = deck.models.all_use_counts()
|
||||
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}
|
||||
deck.models.change(basic, [f.id], cloze, map, map)
|
||||
f.load()
|
||||
|
@ -362,13 +368,16 @@ def test_availOrds():
|
|||
mm.save(m, templates=True)
|
||||
assert not mm.availOrds(m, joinFields(f.fields))
|
||||
# AND
|
||||
t = m["tmpls"][0]
|
||||
t["qfmt"] = "{{#Front}}{{#Back}}{{Front}}{{/Back}}{{/Front}}"
|
||||
mm.save(m, templates=True)
|
||||
assert not mm.availOrds(m, joinFields(f.fields))
|
||||
t = m["tmpls"][0]
|
||||
t["qfmt"] = "{{#Front}}\n{{#Back}}\n{{Front}}\n{{/Back}}\n{{/Front}}"
|
||||
mm.save(m, templates=True)
|
||||
assert not mm.availOrds(m, joinFields(f.fields))
|
||||
# OR
|
||||
t = m["tmpls"][0]
|
||||
t["qfmt"] = "{{Front}}\n{{Back}}"
|
||||
mm.save(m, templates=True)
|
||||
assert mm.availOrds(m, joinFields(f.fields)) == [0]
|
||||
|
|
|
@ -1176,11 +1176,11 @@ QTableView {{ gridline-color: {grid} }}
|
|||
|
||||
def _modelTree(self, root) -> None:
|
||||
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(
|
||||
m["name"],
|
||||
m.name,
|
||||
":/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)
|
||||
|
||||
|
|
|
@ -239,21 +239,12 @@ class CardLayout(QDialog):
|
|||
if len(self.model["tmpls"]) < 2:
|
||||
return showInfo(_("At least one card type is required."))
|
||||
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(
|
||||
a=self.model["tmpls"][idx]["name"], b=cards
|
||||
a=self.model["tmpls"][idx]["name"], b=_("cards")
|
||||
)
|
||||
if not askUser(msg):
|
||||
return
|
||||
if not 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.mm.remTemplate(self.model, self.cards[idx].template())
|
||||
self.redraw()
|
||||
|
||||
def removeColons(self):
|
||||
|
|
|
@ -4,10 +4,12 @@ import collections
|
|||
import re
|
||||
from operator import itemgetter
|
||||
from typing import Optional
|
||||
from typing import List
|
||||
|
||||
import aqt.clayout
|
||||
from anki import stdmodels
|
||||
from anki.lang import _, ngettext
|
||||
from anki.rsbackend import pb
|
||||
from aqt import AnkiQt, gui_hooks
|
||||
from aqt.qt import *
|
||||
from aqt.utils import (
|
||||
|
@ -34,6 +36,7 @@ class Models(QDialog):
|
|||
self.form = aqt.forms.models.Ui_Dialog()
|
||||
self.form.setupUi(self)
|
||||
qconnect(self.form.buttonBox.helpRequested, lambda: openHelp("notetypes"))
|
||||
self.models: List[pb.NoteTypeNameIDUseCount] = []
|
||||
self.setupModels()
|
||||
restoreGeom(self, "models")
|
||||
self.exec_()
|
||||
|
@ -76,13 +79,12 @@ class Models(QDialog):
|
|||
row = self.form.modelsList.currentRow()
|
||||
if row == -1:
|
||||
row = 0
|
||||
self.models = self.col.models.all()
|
||||
self.models.sort(key=itemgetter("name"))
|
||||
self.models = self.col.models.all_use_counts()
|
||||
self.form.modelsList.clear()
|
||||
for m in self.models:
|
||||
mUse = self.mm.useCount(m)
|
||||
mUse = m.use_count
|
||||
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.setCurrentRow(row)
|
||||
|
||||
|
@ -90,7 +92,7 @@ class Models(QDialog):
|
|||
if self.model:
|
||||
self.saveModel()
|
||||
idx = self.form.modelsList.currentRow()
|
||||
self.model = self.models[idx]
|
||||
self.model = self.col.models.get(self.models[idx].id)
|
||||
|
||||
def onAdd(self):
|
||||
m = AddModel(self.mw, self).get()
|
||||
|
|
Loading…
Reference in a new issue