diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index 84977650a..51cea0526 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -310,7 +310,7 @@ class Collection: self._backend.before_upload() self.close(save=False, downgrade=True) - # Object creation helpers + # Object helpers ########################################################################## def get_card(self, id: CardId) -> Card: @@ -347,6 +347,11 @@ class Collection: """Get a new-style notetype object. This is not cached; avoid calling frequently.""" return self._backend.get_notetype(id) + def update_notetype(self, notetype: Notetype) -> OpChanges: + "This may force a full sync; caller is responsible for notifying user." + self.models._remove_from_cache(NotetypeId(notetype.id)) + return self._backend.update_notetype(notetype) + getCard = get_card getNote = get_note diff --git a/pylib/anki/models.py b/pylib/anki/models.py index 8bf365e07..36488094d 100644 --- a/pylib/anki/models.py +++ b/pylib/anki/models.py @@ -12,6 +12,7 @@ from typing import Any, Dict, List, NewType, Optional, Sequence, Tuple, Union import anki # pylint: disable=unused-import import anki._backend.backend_pb2 as _pb +from anki.collection import OpChanges, OpChangesWithId from anki.consts import * from anki.errors import NotFoundError from anki.lang import without_unicode_isolation @@ -232,8 +233,18 @@ class ModelManager: self._remove_from_cache(id) self.col._backend.remove_notetype(id) - def add(self, m: NotetypeDict) -> None: - self.save(m) + def add(self, m: NotetypeDict) -> OpChangesWithId: + "Replaced with add_dict()" + self.ensureNameUnique(m) + out = self.col._backend.add_notetype_legacy(to_json_bytes(m)) + m["id"] = out.id + self._mutate_after_write(m) + return out + + def add_dict(self, m: NotetypeDict) -> OpChangesWithId: + "Notetype needs to be fetched from DB after adding." + self.ensureNameUnique(m) + return self.col._backend.add_notetype_legacy(to_json_bytes(m)) def ensureNameUnique(self, m: NotetypeDict) -> None: existing_id = self.id_for_name(m["name"]) @@ -241,7 +252,7 @@ class ModelManager: m["name"] += "-" + checksum(str(time.time()))[:5] def update(self, m: NotetypeDict, preserve_usn: bool = True) -> None: - "Add or update an existing model. Use .save() instead." + "Add or update an existing model. Use .update_dict() instead." self._remove_from_cache(m["id"]) self.ensureNameUnique(m) m["id"] = self.col._backend.add_or_update_notetype( @@ -250,6 +261,12 @@ class ModelManager: self.setCurrent(m) self._mutate_after_write(m) + def update_dict(self, m: NotetypeDict) -> OpChanges: + "Update a NotetypeDict. Caller will need to re-load notetype if new fields/cards added." + self._remove_from_cache(m["id"]) + self.ensureNameUnique(m) + return self.col._backend.update_notetype_legacy(to_json_bytes(m)) + def _mutate_after_write(self, nt: NotetypeDict) -> None: # existing code expects the note type to be mutated to reflect # the changes made when adding, such as ordinal assignment :-( @@ -273,14 +290,15 @@ class ModelManager: # Copying ################################################## - def copy(self, m: NotetypeDict) -> NotetypeDict: + def copy(self, m: NotetypeDict, add: bool = True) -> NotetypeDict: "Copy, save and return." m2 = copy.deepcopy(m) m2["name"] = without_unicode_isolation( self.col.tr.notetypes_copy(val=m2["name"]) ) m2["id"] = 0 - self.add(m2) + if add: + self.add(m2) return m2 # Fields diff --git a/pylib/anki/notes.py b/pylib/anki/notes.py index c9e2c4bf7..2741bbc47 100644 --- a/pylib/anki/notes.py +++ b/pylib/anki/notes.py @@ -5,7 +5,7 @@ from __future__ import annotations import copy import pprint -from typing import Any, List, NewType, Optional, Sequence, Tuple +from typing import Any, List, NewType, Optional, Sequence, Tuple, Union import anki # pylint: disable=unused-import import anki._backend.backend_pb2 as _pb @@ -30,12 +30,12 @@ class Note: def __init__( self, col: anki.collection.Collection, - model: Optional[NotetypeDict] = None, + model: Optional[Union[NotetypeDict, NotetypeId]] = None, id: Optional[NoteId] = None, ) -> None: assert not (model and id) + notetype_id = model["id"] if isinstance(model, dict) else model self.col = col.weakref() - # self.newlyAdded = False if id: # existing note @@ -43,7 +43,7 @@ class Note: self.load() else: # new note for provided notetype - self._load_from_backend_note(self.col._backend.new_note(model["id"])) + self._load_from_backend_note(self.col._backend.new_note(notetype_id)) def load(self) -> None: n = self.col._backend.get_note(self.id) diff --git a/pylib/anki/stdmodels.py b/pylib/anki/stdmodels.py index 7878b4b1e..a57141d61 100644 --- a/pylib/anki/stdmodels.py +++ b/pylib/anki/stdmodels.py @@ -12,39 +12,15 @@ from anki.utils import from_json_bytes # pylint: disable=no-member StockNotetypeKind = _pb.StockNotetype.Kind -# add-on authors can add ("note type name", function_like_addBasicModel) +# add-on authors can add ("note type name", function) # to this list to have it shown in the add/clone note type screen models: List[Tuple] = [] -def _add_stock_notetype( +def _get_stock_notetype( col: anki.collection.Collection, kind: StockNotetypeKind.V ) -> anki.models.NotetypeDict: - m = from_json_bytes(col._backend.get_stock_notetype_legacy(kind)) - col.models.add(m) - return m - - -def addBasicModel(col: anki.collection.Collection) -> anki.models.NotetypeDict: - return _add_stock_notetype(col, StockNotetypeKind.BASIC) - - -def addBasicTypingModel(col: anki.collection.Collection) -> anki.models.NotetypeDict: - return _add_stock_notetype(col, StockNotetypeKind.BASIC_TYPING) - - -def addForwardReverse(col: anki.collection.Collection) -> anki.models.NotetypeDict: - return _add_stock_notetype(col, StockNotetypeKind.BASIC_AND_REVERSED) - - -def addForwardOptionalReverse( - col: anki.collection.Collection, -) -> anki.models.NotetypeDict: - return _add_stock_notetype(col, StockNotetypeKind.BASIC_OPTIONAL_REVERSED) - - -def addClozeModel(col: anki.collection.Collection) -> anki.models.NotetypeDict: - return _add_stock_notetype(col, StockNotetypeKind.CLOZE) + return from_json_bytes(col._backend.get_stock_notetype_legacy(kind)) def get_stock_notetypes( @@ -54,18 +30,21 @@ def get_stock_notetypes( Tuple[str, Callable[[anki.collection.Collection], anki.models.NotetypeDict]] ] = [] # add standard - for (kind, func) in [ - (StockNotetypeKind.BASIC, addBasicModel), - (StockNotetypeKind.BASIC_TYPING, addBasicTypingModel), - (StockNotetypeKind.BASIC_AND_REVERSED, addForwardReverse), - ( - StockNotetypeKind.BASIC_OPTIONAL_REVERSED, - addForwardOptionalReverse, - ), - (StockNotetypeKind.CLOZE, addClozeModel), + for kind in [ + StockNotetypeKind.BASIC, + StockNotetypeKind.BASIC_TYPING, + StockNotetypeKind.BASIC_AND_REVERSED, + StockNotetypeKind.BASIC_OPTIONAL_REVERSED, + StockNotetypeKind.CLOZE, ]: m = from_json_bytes(col._backend.get_stock_notetype_legacy(kind)) - out.append((m["name"], func)) + + def instance_getter( + col: anki.collection.Collection, + ) -> anki.models.NotetypeDict: + return m # pylint:disable=cell-var-from-loop + + out.append((m["name"], instance_getter)) # add extras from add-ons for (name_or_func, func) in models: if not isinstance(name_or_func, str): @@ -74,3 +53,40 @@ def get_stock_notetypes( name = name_or_func out.append((name, func)) return out + + +# +# Legacy functions that added the notetype before returning it +# + + +def addBasicModel(col: anki.collection.Collection) -> anki.models.NotetypeDict: + nt = _get_stock_notetype(col, StockNotetypeKind.BASIC) + col.models.add(nt) + return nt + + +def addBasicTypingModel(col: anki.collection.Collection) -> anki.models.NotetypeDict: + nt = _get_stock_notetype(col, StockNotetypeKind.BASIC_TYPING) + col.models.add(nt) + return nt + + +def addForwardReverse(col: anki.collection.Collection) -> anki.models.NotetypeDict: + nt = _get_stock_notetype(col, StockNotetypeKind.BASIC_AND_REVERSED) + col.models.add(nt) + return nt + + +def addForwardOptionalReverse( + col: anki.collection.Collection, +) -> anki.models.NotetypeDict: + nt = _get_stock_notetype(col, StockNotetypeKind.BASIC_OPTIONAL_REVERSED) + col.models.add(nt) + return nt + + +def addClozeModel(col: anki.collection.Collection) -> anki.models.NotetypeDict: + nt = _get_stock_notetype(col, StockNotetypeKind.CLOZE) + col.models.add(nt) + return nt diff --git a/qt/aqt/fields.py b/qt/aqt/fields.py index c3012a10d..46de16aea 100644 --- a/qt/aqt/fields.py +++ b/qt/aqt/fields.py @@ -45,7 +45,6 @@ class FieldDialog(QDialog): self.form.buttonBox.button(QDialogButtonBox.Cancel).setAutoDefault(False) self.form.buttonBox.button(QDialogButtonBox.Save).setAutoDefault(False) self.currentIdx: Optional[int] = None - self.oldSortField = self.model["sortf"] self.fillFields() self.setupSignals() self.form.fieldList.setDragDropMode(QAbstractItemView.InternalMove) diff --git a/qt/aqt/models.py b/qt/aqt/models.py index 0226d9786..57b9a396c 100644 --- a/qt/aqt/models.py +++ b/qt/aqt/models.py @@ -11,6 +11,7 @@ from anki.lang import without_unicode_isolation from anki.models import NotetypeDict, NotetypeId, NotetypeNameIdUseCount from anki.notes import Note from aqt import AnkiQt, gui_hooks +from aqt.operations.notetype import add_notetype_legacy from aqt.qt import * from aqt.utils import ( HelpPage, @@ -49,7 +50,7 @@ class Models(QDialog): self.form.buttonBox.helpRequested, lambda: openHelp(HelpPage.ADDING_A_NOTE_TYPE), ) - self.models: List[NotetypeNameIdUseCount] = [] + self.models: Sequence[NotetypeNameIdUseCount] = [] self.setupModels() restoreGeom(self, "models") self.exec_() @@ -100,6 +101,12 @@ class Models(QDialog): self.mw.taskman.with_progress(self.col.models.all_use_counts, on_done, self) maybeHideClose(box) + def refresh_list(self) -> None: + self.mw.query_op( + self.col.models.all_use_counts, + success=self.updateModelsList, + ) + def onRename(self) -> None: nt = self.current_notetype() txt = getText(tr.actions_new_name(), default=nt["name"]) @@ -118,7 +125,7 @@ class Models(QDialog): self.mw.taskman.with_progress(save, on_done, self) - def updateModelsList(self, notetypes: List[NotetypeNameIdUseCount]) -> None: + def updateModelsList(self, notetypes: Sequence[NotetypeNameIdUseCount]) -> None: row = self.form.modelsList.currentRow() if row == -1: row = 0 @@ -138,10 +145,19 @@ class Models(QDialog): def onAdd(self) -> None: m = AddModel(self.mw, self).get() if m: - txt = getText(tr.actions_name(), default=m["name"])[0].replace('"', "") - if txt: - m["name"] = txt - self.saveAndRefresh(m) + # if legacy add-ons already added the notetype, skip adding + if m["id"]: + return + + # prompt for name + text, ok = getText(tr.actions_name(), default=m["name"]) + if not ok or not text.strip(): + return + m["name"] = text + + add_notetype_legacy(parent=self, notetype=m).success( + lambda _: self.refresh_list() + ).run_in_background() def onDelete(self) -> None: if len(self.models) < 2: @@ -258,11 +274,9 @@ class AddModel(QDialog): def accept(self) -> None: model = self.notetypes[self.dialog.models.currentRow()] if isinstance(model, dict): - # add copy to deck - self.model = self.mw.col.models.copy(model) - self.mw.col.models.setCurrent(self.model) + # clone existing + self.model = self.mw.col.models.copy(model, add=False) else: - # create self.model = model(self.col) QDialog.accept(self) diff --git a/qt/aqt/operations/notetype.py b/qt/aqt/operations/notetype.py new file mode 100644 index 000000000..87a805810 --- /dev/null +++ b/qt/aqt/operations/notetype.py @@ -0,0 +1,25 @@ +# Copyright: Ankitects Pty Ltd and contributors +# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +from __future__ import annotations + +from anki.collection import OpChanges, OpChangesWithId +from anki.models import NotetypeDict +from aqt import QWidget +from aqt.operations import CollectionOp + + +def add_notetype_legacy( + *, + parent: QWidget, + notetype: NotetypeDict, +) -> CollectionOp[OpChangesWithId]: + return CollectionOp(parent, lambda col: col.models.add_dict(notetype)) + + +def update_notetype_legacy( + *, + parent: QWidget, + notetype: NotetypeDict, +) -> CollectionOp[OpChanges]: + return CollectionOp(parent, lambda col: col.models.update_dict(notetype)) diff --git a/rslib/backend.proto b/rslib/backend.proto index 76232341f..df2a51293 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -202,6 +202,8 @@ service ConfigService { service NotetypesService { rpc AddNotetype(Notetype) returns (OpChangesWithId); rpc UpdateNotetype(Notetype) returns (OpChanges); + rpc AddNotetypeLegacy(Json) returns (OpChangesWithId); + rpc UpdateNotetypeLegacy(Json) returns (OpChanges); rpc AddOrUpdateNotetype(AddOrUpdateNotetypeIn) returns (NotetypeId); rpc GetStockNotetypeLegacy(StockNotetype) returns (Json); rpc GetNotetype(NotetypeId) returns (Notetype); diff --git a/rslib/src/backend/notetypes.rs b/rslib/src/backend/notetypes.rs index 26133f868..c60aa34d7 100644 --- a/rslib/src/backend/notetypes.rs +++ b/rslib/src/backend/notetypes.rs @@ -26,6 +26,24 @@ impl NotetypesService for Backend { .map(Into::into) } + fn add_notetype_legacy(&self, input: pb::Json) -> Result { + let legacy: NotetypeSchema11 = serde_json::from_slice(&input.json)?; + let mut notetype: Notetype = legacy.into(); + self.with_col(|col| { + Ok(col + .add_notetype(&mut notetype)? + .map(|_| notetype.id.0) + .into()) + }) + } + + fn update_notetype_legacy(&self, input: pb::Json) -> Result { + let legacy: NotetypeSchema11 = serde_json::from_slice(&input.json)?; + let mut notetype: Notetype = legacy.into(); + self.with_col(|col| col.update_notetype(&mut notetype)) + .map(Into::into) + } + fn add_or_update_notetype(&self, input: pb::AddOrUpdateNotetypeIn) -> Result { self.with_col(|col| { let legacy: NotetypeSchema11 = serde_json::from_slice(&input.json)?; diff --git a/rslib/src/notetype/mod.rs b/rslib/src/notetype/mod.rs index fba07ba65..85e787dca 100644 --- a/rslib/src/notetype/mod.rs +++ b/rslib/src/notetype/mod.rs @@ -470,7 +470,8 @@ impl Collection { pub(crate) fn add_notetype_inner(&mut self, notetype: &mut Notetype, usn: Usn) -> Result<()> { notetype.prepare_for_update(None)?; self.ensure_notetype_name_unique(notetype, usn)?; - self.add_notetype_undoable(notetype) + self.add_notetype_undoable(notetype)?; + self.set_current_notetype_id(notetype.id) } /// - Caller must set notetype as modified if appropriate.