Add ability to restore a notetype to its original configuration (#2472)

* Store the original stock notetype kind in the notetype

Will allow us to provide a command to restore a notetype to its default
settings/templates.

* Add a new action to restore a notetype to its original state
This commit is contained in:
Damien Elmes 2023-04-18 14:07:51 +10:00 committed by GitHub
parent e361bb9514
commit dd13e78eca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 382 additions and 79 deletions

View File

@ -45,4 +45,4 @@ good-names =
ip, ip,
[IMPORTS] [IMPORTS]
ignored-modules = anki.*_pb2, anki.sync_pb2, win32file,pywintypes,socket,win32pipe,pyaudio,anki.scheduler_pb2 ignored-modules = anki.*_pb2, anki.sync_pb2, win32file,pywintypes,socket,win32pipe,pyaudio,anki.scheduler_pb2,anki.notetypes_pb2

View File

@ -57,3 +57,8 @@ card-templates-this-will-create-card-proceed =
*[other] This will create { $count } cards. Proceed? *[other] This will create { $count } cards. Proceed?
} }
card-templates-type-boxes-warning = Only one typing box per card template is supported. card-templates-type-boxes-warning = Only one typing box per card template is supported.
card-templates-restore-to-default = Restore to Default
card-templates-restore-to-default-confirmation = This will reset all fields and templates in this notetype to their default
values, and remove any custom styling. Do you wish to proceed?
card-templates-restored-to-default = Notetype has been restored to its original state.

View File

@ -30,6 +30,8 @@ service NotetypesService {
returns (ChangeNotetypeInfo); returns (ChangeNotetypeInfo);
rpc ChangeNotetype(ChangeNotetypeRequest) returns (collection.OpChanges); rpc ChangeNotetype(ChangeNotetypeRequest) returns (collection.OpChanges);
rpc GetFieldNames(NotetypeId) returns (generic.StringList); rpc GetFieldNames(NotetypeId) returns (generic.StringList);
rpc RestoreNotetypeToStock(RestoreNotetypeToStockRequest)
returns (collection.OpChanges);
} }
message NotetypeId { message NotetypeId {
@ -56,12 +58,14 @@ message Notetype {
Kind kind = 1; Kind kind = 1;
uint32 sort_field_idx = 2; uint32 sort_field_idx = 2;
string css = 3; string css = 3;
/// This is now stored separately; retrieve with DefaultsForAdding() // This is now stored separately; retrieve with DefaultsForAdding()
int64 target_deck_id_unused = 4; int64 target_deck_id_unused = 4;
string latex_pre = 5; string latex_pre = 5;
string latex_post = 6; string latex_post = 6;
bool latex_svg = 7; bool latex_svg = 7;
repeated CardRequirement reqs = 8; repeated CardRequirement reqs = 8;
// Only set on notetypes created with Anki 2.1.62+.
StockNotetype.OriginalStockKind original_stock_kind = 9;
bytes other = 255; bytes other = 255;
} }
@ -119,12 +123,24 @@ message AddOrUpdateNotetypeRequest {
message StockNotetype { message StockNotetype {
enum Kind { enum Kind {
BASIC = 0; KIND_BASIC = 0;
BASIC_AND_REVERSED = 1; KIND_BASIC_AND_REVERSED = 1;
BASIC_OPTIONAL_REVERSED = 2; KIND_BASIC_OPTIONAL_REVERSED = 2;
BASIC_TYPING = 3; KIND_BASIC_TYPING = 3;
CLOZE = 4; KIND_CLOZE = 4;
IMAGE_OCCLUSION = 5; }
// This is decoupled from Kind to allow us to evolve notetypes over time
// (eg an older notetype might require different JS), and allow us to store
// a type even for notetypes that we don't add by default. Code should not
// assume that the entries here are always +1 from Kind.
enum OriginalStockKind {
ORIGINAL_STOCK_KIND_UNKNOWN = 0;
ORIGINAL_STOCK_KIND_BASIC = 1;
ORIGINAL_STOCK_KIND_BASIC_AND_REVERSED = 2;
ORIGINAL_STOCK_KIND_BASIC_OPTIONAL_REVERSED = 3;
ORIGINAL_STOCK_KIND_BASIC_TYPING = 4;
ORIGINAL_STOCK_KIND_CLOZE = 5;
ORIGINAL_STOCK_KIND_IMAGE_OCCLUSION = 6;
} }
Kind kind = 1; Kind kind = 1;
@ -185,3 +201,10 @@ message ChangeNotetypeInfo {
ChangeNotetypeRequest input = 5; ChangeNotetypeRequest input = 5;
string old_notetype_name = 6; string old_notetype_name = 6;
} }
message RestoreNotetypeToStockRequest {
NotetypeId notetype_id = 1;
// Older notetypes did not store their original stock kind, so we allow the UI
// to pass in an override to use when missing, or for tests.
optional StockNotetype.Kind force_kind = 2;
}

View File

@ -181,7 +181,7 @@ class ModelManager(DeprecatedNamesMixin):
"Create a new model, and return it." "Create a new model, and return it."
# caller should call save() after modifying # caller should call save() after modifying
notetype = from_json_bytes( notetype = from_json_bytes(
self.col._backend.get_stock_notetype_legacy(StockNotetypeKind.BASIC) self.col._backend.get_stock_notetype_legacy(StockNotetypeKind.KIND_BASIC)
) )
notetype["flds"] = [] notetype["flds"] = []
notetype["tmpls"] = [] notetype["tmpls"] = []
@ -277,7 +277,7 @@ class ModelManager(DeprecatedNamesMixin):
def new_field(self, name: str) -> FieldDict: def new_field(self, name: str) -> FieldDict:
assert isinstance(name, str) assert isinstance(name, str)
notetype = from_json_bytes( notetype = from_json_bytes(
self.col._backend.get_stock_notetype_legacy(StockNotetypeKind.BASIC) self.col._backend.get_stock_notetype_legacy(StockNotetypeKind.KIND_BASIC)
) )
field = notetype["flds"][0] field = notetype["flds"][0]
field["name"] = name field["name"] = name
@ -321,7 +321,7 @@ class ModelManager(DeprecatedNamesMixin):
def new_template(self, name: str) -> TemplateDict: def new_template(self, name: str) -> TemplateDict:
notetype = from_json_bytes( notetype = from_json_bytes(
self.col._backend.get_stock_notetype_legacy(StockNotetypeKind.BASIC) self.col._backend.get_stock_notetype_legacy(StockNotetypeKind.KIND_BASIC)
) )
template = notetype["tmpls"][0] template = notetype["tmpls"][0]
template["name"] = name template["name"] = name
@ -393,6 +393,16 @@ and notes.mid = ? and cards.ord = ?""",
op_bytes = self.col._backend.change_notetype_raw(input.SerializeToString()) op_bytes = self.col._backend.change_notetype_raw(input.SerializeToString())
return OpChanges.FromString(op_bytes) return OpChanges.FromString(op_bytes)
def restore_notetype_to_stock(
self, notetype_id: NotetypeId, force_kind: StockNotetypeKind.V | None
) -> OpChanges:
msg = notetypes_pb2.RestoreNotetypeToStockRequest(
notetype_id=notetypes_pb2.NotetypeId(ntid=notetype_id),
)
if force_kind is not None:
msg.force_kind = force_kind
return self.col._backend.restore_notetype_to_stock(msg)
# legacy API - used by unit tests and add-ons # legacy API - used by unit tests and add-ons
def change( # pylint: disable=invalid-name def change( # pylint: disable=invalid-name

View File

@ -31,13 +31,13 @@ def get_stock_notetypes(
out: list[ out: list[
tuple[str, Callable[[anki.collection.Collection], anki.models.NotetypeDict]] tuple[str, Callable[[anki.collection.Collection], anki.models.NotetypeDict]]
] = [] ] = []
# add standard # add standard - this order should match the one in notetypes.proto
for kind in [ for kind in [
StockNotetypeKind.BASIC, StockNotetypeKind.KIND_BASIC,
StockNotetypeKind.BASIC_TYPING, StockNotetypeKind.KIND_BASIC_AND_REVERSED,
StockNotetypeKind.BASIC_AND_REVERSED, StockNotetypeKind.KIND_BASIC_OPTIONAL_REVERSED,
StockNotetypeKind.BASIC_OPTIONAL_REVERSED, StockNotetypeKind.KIND_BASIC_TYPING,
StockNotetypeKind.CLOZE, StockNotetypeKind.KIND_CLOZE,
]: ]:
note_type = from_json_bytes(col._backend.get_stock_notetype_legacy(kind)) note_type = from_json_bytes(col._backend.get_stock_notetype_legacy(kind))
@ -65,7 +65,7 @@ def get_stock_notetypes(
def _legacy_add_basic_model( def _legacy_add_basic_model(
col: anki.collection.Collection, col: anki.collection.Collection,
) -> anki.models.NotetypeDict: ) -> anki.models.NotetypeDict:
note_type = _get_stock_notetype(col, StockNotetypeKind.BASIC) note_type = _get_stock_notetype(col, StockNotetypeKind.KIND_BASIC)
col.models.add(note_type) col.models.add(note_type)
return note_type return note_type
@ -73,7 +73,7 @@ def _legacy_add_basic_model(
def _legacy_add_basic_typing_model( def _legacy_add_basic_typing_model(
col: anki.collection.Collection, col: anki.collection.Collection,
) -> anki.models.NotetypeDict: ) -> anki.models.NotetypeDict:
note_type = _get_stock_notetype(col, StockNotetypeKind.BASIC_TYPING) note_type = _get_stock_notetype(col, StockNotetypeKind.KIND_BASIC_TYPING)
col.models.add(note_type) col.models.add(note_type)
return note_type return note_type
@ -81,7 +81,7 @@ def _legacy_add_basic_typing_model(
def _legacy_add_forward_reverse( def _legacy_add_forward_reverse(
col: anki.collection.Collection, col: anki.collection.Collection,
) -> anki.models.NotetypeDict: ) -> anki.models.NotetypeDict:
note_type = _get_stock_notetype(col, StockNotetypeKind.BASIC_AND_REVERSED) note_type = _get_stock_notetype(col, StockNotetypeKind.KIND_BASIC_AND_REVERSED)
col.models.add(note_type) col.models.add(note_type)
return note_type return note_type
@ -89,7 +89,7 @@ def _legacy_add_forward_reverse(
def _legacy_add_forward_optional_reverse( def _legacy_add_forward_optional_reverse(
col: anki.collection.Collection, col: anki.collection.Collection,
) -> anki.models.NotetypeDict: ) -> anki.models.NotetypeDict:
note_type = _get_stock_notetype(col, StockNotetypeKind.BASIC_OPTIONAL_REVERSED) note_type = _get_stock_notetype(col, StockNotetypeKind.KIND_BASIC_OPTIONAL_REVERSED)
col.models.add(note_type) col.models.add(note_type)
return note_type return note_type
@ -97,7 +97,7 @@ def _legacy_add_forward_optional_reverse(
def _legacy_add_cloze_model( def _legacy_add_cloze_model(
col: anki.collection.Collection, col: anki.collection.Collection,
) -> anki.models.NotetypeDict: ) -> anki.models.NotetypeDict:
note_type = _get_stock_notetype(col, StockNotetypeKind.CLOZE) note_type = _get_stock_notetype(col, StockNotetypeKind.KIND_CLOZE)
col.models.add(note_type) col.models.add(note_type)
return note_type return note_type

View File

@ -1,20 +1,25 @@
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import annotations
import json import json
import re import re
from concurrent.futures import Future from concurrent.futures import Future
from typing import Any, Match, Optional from typing import Any, Match, Optional, cast
import aqt import aqt
import aqt.forms import aqt.forms
import aqt.operations import aqt.operations
from anki import stdmodels
from anki.collection import OpChanges from anki.collection import OpChanges
from anki.consts import * from anki.consts import *
from anki.lang import without_unicode_isolation from anki.lang import without_unicode_isolation
from anki.notes import Note from anki.notes import Note
from anki.notetypes_pb2 import StockNotetype
from aqt import AnkiQt, gui_hooks from aqt import AnkiQt, gui_hooks
from aqt.forms import browserdisp from aqt.forms import browserdisp
from aqt.operations.notetype import update_notetype_legacy from aqt.operations.notetype import restore_notetype_to_stock, update_notetype_legacy
from aqt.qt import * from aqt.qt import *
from aqt.schema_change_tracker import ChangeTracker from aqt.schema_change_tracker import ChangeTracker
from aqt.sound import av_player, play_clicked_audio from aqt.sound import av_player, play_clicked_audio
@ -696,6 +701,31 @@ class CardLayout(QDialog):
self.ord = len(self.templates) - 1 self.ord = len(self.templates) - 1
self.redraw_everything() self.redraw_everything()
def on_restore_to_default(
self, force_kind: StockNotetype.Kind.V | None = None
) -> None:
if force_kind is None and not self.model.get("originalStockKind", 0):
SelectStockNotetype(
mw=self.mw,
on_success=lambda kind: self.on_restore_to_default(force_kind=kind),
parent=self,
)
return
if not askUser(
tr.card_templates_restore_to_default_confirmation(), defaultno=True
):
return
def on_success(changes: OpChanges) -> None:
self.change_tracker.set_unchanged()
self.close()
showInfo(tr.card_templates_restored_to_default(), parent=self.mw)
restore_notetype_to_stock(
parent=self, notetype_id=self.model["id"], force_kind=force_kind
).success(on_success).run_in_background()
def onFlip(self) -> None: def onFlip(self) -> None:
old = self.current_template() old = self.current_template()
self._flipQA(old, old) self._flipQA(old, old)
@ -717,6 +747,14 @@ class CardLayout(QDialog):
a = m.addAction(tr.card_templates_add_card_type()) a = m.addAction(tr.card_templates_add_card_type())
qconnect(a.triggered, self.onAddCard) qconnect(a.triggered, self.onAddCard)
a = m.addAction(
tr.actions_with_ellipsis(action=tr.card_templates_restore_to_default())
)
qconnect(
a.triggered,
lambda: self.on_restore_to_default(), # pylint: disable=unnecessary-lambda
)
a = m.addAction(tr.card_templates_remove_card_type()) a = m.addAction(tr.card_templates_remove_card_type())
qconnect(a.triggered, self.onRemove) qconnect(a.triggered, self.onRemove)
@ -869,3 +907,42 @@ class CardLayout(QDialog):
def onHelp(self) -> None: def onHelp(self) -> None:
openHelp(HelpPage.TEMPLATES) openHelp(HelpPage.TEMPLATES)
class SelectStockNotetype(QDialog):
def __init__(
self,
mw: AnkiQt,
on_success: Callable[[StockNotetype.Kind.V], None],
parent: QWidget,
) -> None:
self.mw = mw
QDialog.__init__(self, parent, Qt.WindowType.Window)
self.dialog = aqt.forms.addmodel.Ui_Dialog()
self.dialog.setupUi(self)
self.setWindowTitle("Anki")
self.setWindowModality(Qt.WindowModality.ApplicationModal)
disable_help_button(self)
stock_types = stdmodels.get_stock_notetypes(mw.col)
for name, func in stock_types:
item = QListWidgetItem(name)
self.dialog.models.addItem(item)
self.dialog.models.setCurrentRow(0)
# the list widget will swallow the enter key
s = QShortcut(QKeySequence("Return"), self)
qconnect(s.activated, self.accept)
# help
# self.dialog.buttonBox.standardButton(QDialogButtonBox.StandardButton.Help).
self.on_success = on_success
self.show()
def reject(self) -> None:
QDialog.reject(self)
def accept(self) -> None:
kind = cast(StockNotetype.Kind.V, self.dialog.models.currentRow())
QDialog.accept(self)
# On Mac, we need to allow time for the existing modal to close or
# Qt gets confused.
self.mw.progress.single_shot(100, lambda: self.on_success(kind), True)

View File

@ -5,6 +5,7 @@ from __future__ import annotations
from anki.collection import OpChanges, OpChangesWithId from anki.collection import OpChanges, OpChangesWithId
from anki.models import ChangeNotetypeRequest, NotetypeDict, NotetypeId from anki.models import ChangeNotetypeRequest, NotetypeDict, NotetypeId
from anki.stdmodels import StockNotetypeKind
from aqt.operations import CollectionOp from aqt.operations import CollectionOp
from aqt.qt import QWidget from aqt.qt import QWidget
@ -37,3 +38,12 @@ def change_notetype_of_notes(
*, parent: QWidget, input: ChangeNotetypeRequest *, parent: QWidget, input: ChangeNotetypeRequest
) -> CollectionOp[OpChanges]: ) -> CollectionOp[OpChanges]:
return CollectionOp(parent, lambda col: col.models.change_notetype_of_notes(input)) return CollectionOp(parent, lambda col: col.models.change_notetype_of_notes(input))
def restore_notetype_to_stock(
*, parent: QWidget, notetype_id: NotetypeId, force_kind: StockNotetypeKind.V | None
) -> CollectionOp[OpChanges]:
return CollectionOp(
parent,
lambda col: col.models.restore_notetype_to_stock(notetype_id, force_kind),
)

View File

@ -34,3 +34,6 @@ class ChangeTracker:
def changed(self) -> bool: def changed(self) -> bool:
return self._changed != Change.NO_CHANGE return self._changed != Change.NO_CHANGE
def set_unchanged(self) -> None:
self._changed = Change.NO_CHANGE

View File

@ -3,7 +3,8 @@
use super::Backend; use super::Backend;
use crate::config::get_aux_notetype_config_key; use crate::config::get_aux_notetype_config_key;
use crate::notetype::all_stock_notetypes; use crate::notetype::stock::get_stock_notetype;
use crate::notetype::stock::StockKind;
use crate::notetype::ChangeNotetypeInput; use crate::notetype::ChangeNotetypeInput;
use crate::notetype::Notetype; use crate::notetype::Notetype;
use crate::notetype::NotetypeChangeInfo; use crate::notetype::NotetypeChangeInfo;
@ -81,10 +82,7 @@ impl NotetypesService for Backend {
&self, &self,
input: pb::notetypes::StockNotetype, input: pb::notetypes::StockNotetype,
) -> Result<pb::generic::Json> { ) -> Result<pb::generic::Json> {
// fixme: use individual functions instead of full vec let nt = get_stock_notetype(input.kind(), &self.tr);
let mut all = all_stock_notetypes(&self.tr);
let idx = (input.kind as usize).min(all.len() - 1);
let nt = all.swap_remove(idx);
let schema11: NotetypeSchema11 = nt.into(); let schema11: NotetypeSchema11 = nt.into();
serde_json::to_vec(&schema11) serde_json::to_vec(&schema11)
.map_err(Into::into) .map_err(Into::into)
@ -208,6 +206,20 @@ impl NotetypesService for Backend {
self.with_col(|col| col.storage.get_field_names(input.into())) self.with_col(|col| col.storage.get_field_names(input.into()))
.map(Into::into) .map(Into::into)
} }
fn restore_notetype_to_stock(
&self,
input: pb::notetypes::RestoreNotetypeToStockRequest,
) -> Result<pb::collection::OpChanges> {
let force_kind = input.force_kind.and_then(StockKind::from_i32);
self.with_col(|col| {
col.restore_notetype_to_stock(
input.notetype_id.or_invalid("missing notetype id")?.into(),
force_kind,
)
.map(Into::into)
})
}
} }
impl From<pb::notetypes::Notetype> for Notetype { impl From<pb::notetypes::Notetype> for Notetype {

View File

@ -10,13 +10,15 @@ use regex::Regex;
use crate::io::metadata; use crate::io::metadata;
use crate::io::read_file; use crate::io::read_file;
use crate::media::MediaManager; use crate::media::MediaManager;
use crate::notetype::stock::empty_stock;
use crate::notetype::CardGenContext; use crate::notetype::CardGenContext;
use crate::notetype::Notetype; use crate::notetype::Notetype;
use crate::notetype::NotetypeConfig; use crate::notetype::NotetypeKind;
use crate::pb::image_occlusion::image_cloze_note_response::Value; use crate::pb::image_occlusion::image_cloze_note_response::Value;
use crate::pb::image_occlusion::ImageClozeNote; use crate::pb::image_occlusion::ImageClozeNote;
use crate::pb::image_occlusion::ImageClozeNoteResponse; use crate::pb::image_occlusion::ImageClozeNoteResponse;
pub use crate::pb::image_occlusion::ImageData; pub use crate::pb::image_occlusion::ImageData;
use crate::pb::notetypes::stock_notetype::OriginalStockKind;
use crate::prelude::*; use crate::prelude::*;
impl Collection { impl Collection {
@ -101,7 +103,7 @@ impl Collection {
fn add_io_notetype(&mut self) -> Result<()> { fn add_io_notetype(&mut self) -> Result<()> {
let usn = self.usn()?; let usn = self.usn()?;
let mut nt = self.image_occlusion_notetype(); let mut nt = image_occlusion_notetype(&self.tr);
self.add_notetype_inner(&mut nt, usn, false)?; self.add_notetype_inner(&mut nt, usn, false)?;
Ok(()) Ok(())
} }
@ -187,28 +189,28 @@ impl Collection {
Ok(false) Ok(false)
} }
}
fn image_occlusion_notetype(&mut self) -> Notetype { pub(crate) fn image_occlusion_notetype(tr: &I18n) -> Notetype {
let tr = &self.tr; const IMAGE_CLOZE_CSS: &str = include_str!("image_occlusion_styling.css");
const IMAGE_CLOZE_CSS: &str = include_str!("image_occlusion_styling.css"); let mut nt = empty_stock(
let mut nt = Notetype { NotetypeKind::Cloze,
name: tr.notetypes_image_occlusion_name().into(), OriginalStockKind::ImageOcclusion,
config: NotetypeConfig::new_cloze(), tr.notetypes_image_occlusion_name(),
..Default::default() );
}; nt.config.css = IMAGE_CLOZE_CSS.to_string();
nt.config.css = IMAGE_CLOZE_CSS.to_string(); let occlusion = tr.notetypes_occlusion();
let occlusion = tr.notetypes_occlusion(); nt.add_field(occlusion.as_ref());
nt.add_field(occlusion.as_ref()); let image = tr.notetypes_image();
let image = tr.notetypes_image(); nt.add_field(image.as_ref());
nt.add_field(image.as_ref()); let header = tr.notetypes_header();
let header = tr.notetypes_header(); nt.add_field(header.as_ref());
nt.add_field(header.as_ref()); let back_extra = tr.notetypes_back_extra_field();
let back_extra = tr.notetypes_back_extra_field(); nt.add_field(back_extra.as_ref());
nt.add_field(back_extra.as_ref()); let comments = tr.notetypes_comments_field();
let comments = tr.notetypes_comments_field(); nt.add_field(comments.as_ref());
nt.add_field(comments.as_ref()); let qfmt = format!(
let qfmt = format!( "<div style=\"display: none\">{{{{cloze:{}}}}}</div>
"<div style=\"display: none\">{{{{cloze:{}}}}}</div>
<div id=container> <div id=container>
{{{{{}}}}} {{{{{}}}}}
<canvas id=\"canvas\" class=\"image-occlusion-canvas\"></canvas> <canvas id=\"canvas\" class=\"image-occlusion-canvas\"></canvas>
@ -222,25 +224,24 @@ try {{
}} }}
</script> </script>
", ",
occlusion, occlusion,
image, image,
tr.notetypes_error_loading_image_occlusion(), tr.notetypes_error_loading_image_occlusion(),
); );
let afmt = format!( let afmt = format!(
"{{{{{}}}}} "{{{{{}}}}}
{} {}
<button id=\"toggle\">{}</button> <button id=\"toggle\">{}</button>
<br> <br>
{{{{{}}}}} {{{{{}}}}}
<br> <br>
{{{{{}}}}}", {{{{{}}}}}",
header, header,
qfmt, qfmt,
tr.notetypes_toggle_masks(), tr.notetypes_toggle_masks(),
back_extra, back_extra,
comments, comments,
); );
nt.add_template(nt.name.clone(), qfmt, afmt); nt.add_template(nt.name.clone(), qfmt, afmt);
nt nt
}
} }

View File

@ -7,6 +7,7 @@ mod emptycards;
mod fields; mod fields;
mod notetypechange; mod notetypechange;
mod render; mod render;
mod restore;
mod schema11; mod schema11;
mod schemachange; mod schemachange;
pub(crate) mod stock; pub(crate) mod stock;

View File

@ -0,0 +1,93 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use crate::notetype::stock::get_original_stock_notetype;
use crate::notetype::stock::StockKind;
use crate::pb::notetypes::stock_notetype::Kind;
use crate::pb::notetypes::stock_notetype::OriginalStockKind;
use crate::prelude::*;
impl Collection {
/// If force_kind is not Unknown, it will be used in preference to the kind
/// stored in the notetype. If Unknown, and the kind stored in the
/// notetype is also Unknown, an error will be returned.
pub(crate) fn restore_notetype_to_stock(
&mut self,
notetype_id: NotetypeId,
force_kind: Option<StockKind>,
) -> Result<OpOutput<()>> {
let mut nt = self
.storage
.get_notetype(notetype_id)?
.or_not_found(notetype_id)?;
let stock_kind = match (nt.config.original_stock_kind(), force_kind) {
(_, Some(force_kind)) => match force_kind {
Kind::Basic => OriginalStockKind::Basic,
Kind::BasicAndReversed => OriginalStockKind::BasicAndReversed,
Kind::BasicOptionalReversed => OriginalStockKind::BasicOptionalReversed,
Kind::BasicTyping => OriginalStockKind::BasicTyping,
Kind::Cloze => OriginalStockKind::Cloze,
},
(stock, _) => stock,
};
if stock_kind == OriginalStockKind::Unknown {
invalid_input!("unknown original notetype kind");
}
let mut stock_nt = get_original_stock_notetype(stock_kind, &self.tr)?;
for (idx, item) in stock_nt.templates.iter_mut().enumerate() {
item.ord = Some(idx as u32);
}
nt.templates = stock_nt.templates;
for (idx, item) in stock_nt.fields.iter_mut().enumerate() {
item.ord = Some(idx as u32);
}
nt.fields = stock_nt.fields;
nt.config.css = stock_nt.config.css;
if force_kind.is_some() {
nt.config.original_stock_kind = stock_kind as i32;
nt.config.kind = stock_nt.config.kind;
}
self.update_notetype(&mut nt, false)
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn adding_and_removing_fields_and_templates() -> Result<()> {
let mut col = Collection::new();
let nt = col.get_notetype_by_name("Basic")?.unwrap();
let note = NoteAdder::basic(&mut col)
.fields(&["front", "back"])
.add(&mut col);
col.restore_notetype_to_stock(nt.id, Some(StockKind::BasicOptionalReversed))?;
let note = col.storage.get_note(note.id)?.unwrap();
assert_eq!(note.fields(), &["front", "back", ""]);
assert_eq!(
col.storage.db_scalar::<u32>("select count(*) from cards")?,
1
);
col.restore_notetype_to_stock(nt.id, Some(StockKind::BasicAndReversed))?;
let note = col.storage.get_note(note.id)?.unwrap();
assert_eq!(note.fields(), &["front", "back"]);
assert_eq!(
col.storage.db_scalar::<u32>("select count(*) from cards")?,
2
);
col.restore_notetype_to_stock(nt.id, Some(StockKind::Cloze))?;
let note = col.storage.get_note(note.id)?.unwrap();
assert_eq!(note.fields(), &["front", "back"]);
assert_eq!(
col.storage.db_scalar::<u32>("select count(*) from cards")?,
1
);
Ok(())
}
}

View File

@ -23,6 +23,7 @@ use crate::notetype::NotetypeConfig;
use crate::serde::default_on_invalid; use crate::serde::default_on_invalid;
use crate::serde::deserialize_bool_from_anything; use crate::serde::deserialize_bool_from_anything;
use crate::serde::deserialize_number_from_string; use crate::serde::deserialize_number_from_string;
use crate::serde::is_default;
use crate::timestamp::TimestampSecs; use crate::timestamp::TimestampSecs;
use crate::types::Usn; use crate::types::Usn;
@ -59,6 +60,8 @@ pub struct NotetypeSchema11 {
pub latexsvg: bool, pub latexsvg: bool,
#[serde(default, deserialize_with = "default_on_invalid")] #[serde(default, deserialize_with = "default_on_invalid")]
pub(crate) req: CardRequirementsSchema11, pub(crate) req: CardRequirementsSchema11,
#[serde(default, skip_serializing_if = "is_default")]
pub(crate) original_stock_kind: i32,
#[serde(flatten)] #[serde(flatten)]
pub(crate) other: HashMap<String, Value>, pub(crate) other: HashMap<String, Value>,
} }
@ -103,6 +106,7 @@ impl From<NotetypeSchema11> for Notetype {
latex_post: nt.latex_post, latex_post: nt.latex_post,
latex_svg: nt.latexsvg, latex_svg: nt.latexsvg,
reqs: nt.req.0.into_iter().map(Into::into).collect(), reqs: nt.req.0.into_iter().map(Into::into).collect(),
original_stock_kind: nt.original_stock_kind,
other: other_to_bytes(&nt.other), other: other_to_bytes(&nt.other),
}, },
fields: nt.flds.into_iter().map(Into::into).collect(), fields: nt.flds.into_iter().map(Into::into).collect(),
@ -160,6 +164,7 @@ impl From<Notetype> for NotetypeSchema11 {
latex_post: c.latex_post, latex_post: c.latex_post,
latexsvg: c.latex_svg, latexsvg: c.latex_svg,
req: CardRequirementsSchema11(c.reqs.into_iter().map(Into::into).collect()), req: CardRequirementsSchema11(c.reqs.into_iter().map(Into::into).collect()),
original_stock_kind: c.original_stock_kind,
other: bytes_to_other(&c.other), other: bytes_to_other(&c.other),
} }
} }
@ -167,7 +172,13 @@ impl From<Notetype> for NotetypeSchema11 {
/// See [crate::deckconfig::schema11::clear_other_duplicates()]. /// See [crate::deckconfig::schema11::clear_other_duplicates()].
fn clear_other_field_duplicates(other: &mut HashMap<String, Value>) { fn clear_other_field_duplicates(other: &mut HashMap<String, Value>) {
for key in &["description", "plainText", "collapsed", "excludeFromSearch"] { for key in &[
"description",
"plainText",
"collapsed",
"excludeFromSearch",
"originalStockKind",
] {
other.remove(*key); other.remove(*key);
} }
} }

View File

@ -6,8 +6,13 @@ use crate::config::ConfigEntry;
use crate::config::ConfigKey; use crate::config::ConfigKey;
use crate::error::Result; use crate::error::Result;
use crate::i18n::I18n; use crate::i18n::I18n;
use crate::image_occlusion::imagedata::image_occlusion_notetype;
use crate::invalid_input;
use crate::notetype::Notetype; use crate::notetype::Notetype;
use crate::pb::notetypes::notetype::config::Kind as NotetypeKind;
use crate::pb::notetypes::stock_notetype::Kind; use crate::pb::notetypes::stock_notetype::Kind;
pub(crate) use crate::pb::notetypes::stock_notetype::Kind as StockKind;
use crate::pb::notetypes::stock_notetype::OriginalStockKind;
use crate::storage::SqliteStorage; use crate::storage::SqliteStorage;
use crate::timestamp::TimestampSecs; use crate::timestamp::TimestampSecs;
@ -16,7 +21,7 @@ impl SqliteStorage {
for (idx, mut nt) in all_stock_notetypes(tr).into_iter().enumerate() { for (idx, mut nt) in all_stock_notetypes(tr).into_iter().enumerate() {
nt.prepare_for_update(None, true)?; nt.prepare_for_update(None, true)?;
self.add_notetype(&mut nt)?; self.add_notetype(&mut nt)?;
if idx == Kind::Basic as usize { if idx == 0 {
self.set_config_entry(&ConfigEntry::boxed( self.set_config_entry(&ConfigEntry::boxed(
ConfigKey::CurrentNotetypeId.into(), ConfigKey::CurrentNotetypeId.into(),
serde_json::to_vec(&nt.id)?, serde_json::to_vec(&nt.id)?,
@ -29,7 +34,8 @@ impl SqliteStorage {
} }
} }
// if changing this, make sure to update StockNotetype enum // If changing this, make sure to update StockNotetype enum. Other parts of the
// code expect the order here to be the same as the enum.
pub fn all_stock_notetypes(tr: &I18n) -> Vec<Notetype> { pub fn all_stock_notetypes(tr: &I18n) -> Vec<Notetype> {
vec![ vec![
basic(tr), basic(tr),
@ -45,11 +51,55 @@ fn fieldref<S: AsRef<str>>(name: S) -> String {
format!("{{{{{}}}}}", name.as_ref()) format!("{{{{{}}}}}", name.as_ref())
} }
pub(crate) fn basic(tr: &I18n) -> Notetype { /// Create an empty notetype with a given name and stock kind.
let mut nt = Notetype { pub(crate) fn empty_stock(
name: tr.notetypes_basic_name().into(), nt_kind: NotetypeKind,
original_stock_kind: OriginalStockKind,
name: impl Into<String>,
) -> Notetype {
Notetype {
name: name.into(),
config: NotetypeConfig {
kind: nt_kind as i32,
original_stock_kind: original_stock_kind as i32,
..if nt_kind == NotetypeKind::Cloze {
NotetypeConfig::new_cloze()
} else {
NotetypeConfig::new()
}
},
..Default::default() ..Default::default()
}; }
}
pub(crate) fn get_stock_notetype(kind: StockKind, tr: &I18n) -> Notetype {
match kind {
Kind::Basic => basic(tr),
Kind::BasicAndReversed => basic_forward_reverse(tr),
Kind::BasicOptionalReversed => basic_optional_reverse(tr),
Kind::BasicTyping => basic_typing(tr),
Kind::Cloze => cloze(tr),
}
}
pub(crate) fn get_original_stock_notetype(kind: OriginalStockKind, tr: &I18n) -> Result<Notetype> {
Ok(match kind {
OriginalStockKind::Unknown => invalid_input!("original stock kind not provided"),
OriginalStockKind::Basic => basic(tr),
OriginalStockKind::BasicAndReversed => basic_forward_reverse(tr),
OriginalStockKind::BasicOptionalReversed => basic_optional_reverse(tr),
OriginalStockKind::BasicTyping => basic_typing(tr),
OriginalStockKind::Cloze => cloze(tr),
OriginalStockKind::ImageOcclusion => image_occlusion_notetype(tr),
})
}
pub(crate) fn basic(tr: &I18n) -> Notetype {
let mut nt = empty_stock(
NotetypeKind::Normal,
OriginalStockKind::Basic,
tr.notetypes_basic_name(),
);
let front = tr.notetypes_front_field(); let front = tr.notetypes_front_field();
let back = tr.notetypes_back_field(); let back = tr.notetypes_back_field();
nt.add_field(front.as_ref()); nt.add_field(front.as_ref());
@ -68,6 +118,7 @@ pub(crate) fn basic(tr: &I18n) -> Notetype {
pub(crate) fn basic_typing(tr: &I18n) -> Notetype { pub(crate) fn basic_typing(tr: &I18n) -> Notetype {
let mut nt = basic(tr); let mut nt = basic(tr);
nt.config.original_stock_kind = StockKind::BasicTyping as i32;
nt.name = tr.notetypes_basic_type_answer_name().into(); nt.name = tr.notetypes_basic_type_answer_name().into();
let front = tr.notetypes_front_field(); let front = tr.notetypes_front_field();
let back = tr.notetypes_back_field(); let back = tr.notetypes_back_field();
@ -83,6 +134,7 @@ pub(crate) fn basic_typing(tr: &I18n) -> Notetype {
pub(crate) fn basic_forward_reverse(tr: &I18n) -> Notetype { pub(crate) fn basic_forward_reverse(tr: &I18n) -> Notetype {
let mut nt = basic(tr); let mut nt = basic(tr);
nt.config.original_stock_kind = StockKind::BasicAndReversed as i32;
nt.name = tr.notetypes_basic_reversed_name().into(); nt.name = tr.notetypes_basic_reversed_name().into();
let front = tr.notetypes_front_field(); let front = tr.notetypes_front_field();
let back = tr.notetypes_back_field(); let back = tr.notetypes_back_field();
@ -100,6 +152,7 @@ pub(crate) fn basic_forward_reverse(tr: &I18n) -> Notetype {
pub(crate) fn basic_optional_reverse(tr: &I18n) -> Notetype { pub(crate) fn basic_optional_reverse(tr: &I18n) -> Notetype {
let mut nt = basic_forward_reverse(tr); let mut nt = basic_forward_reverse(tr);
nt.config.original_stock_kind = StockKind::BasicOptionalReversed as i32;
nt.name = tr.notetypes_basic_optional_reversed_name().into(); nt.name = tr.notetypes_basic_optional_reversed_name().into();
let addrev = tr.notetypes_add_reverse_field(); let addrev = tr.notetypes_add_reverse_field();
nt.add_field(addrev.as_ref()); nt.add_field(addrev.as_ref());
@ -109,11 +162,11 @@ pub(crate) fn basic_optional_reverse(tr: &I18n) -> Notetype {
} }
pub(crate) fn cloze(tr: &I18n) -> Notetype { pub(crate) fn cloze(tr: &I18n) -> Notetype {
let mut nt = Notetype { let mut nt = empty_stock(
name: tr.notetypes_cloze_name().into(), NotetypeKind::Cloze,
config: NotetypeConfig::new_cloze(), OriginalStockKind::Cloze,
..Default::default() tr.notetypes_cloze_name(),
}; );
let text = tr.notetypes_text_field(); let text = tr.notetypes_text_field();
nt.add_field(text.as_ref()); nt.add_field(text.as_ref());
let back_extra = tr.notetypes_back_extra_field(); let back_extra = tr.notetypes_back_extra_field();

View File

@ -20,6 +20,10 @@ where
Ok(T::deserialize(v).unwrap_or_default()) Ok(T::deserialize(v).unwrap_or_default())
} }
pub(crate) fn is_default<T: Default + PartialEq>(t: &T) -> bool {
*t == Default::default()
}
pub(crate) fn deserialize_int_from_number<'de, T, D>(deserializer: D) -> Result<T, D::Error> pub(crate) fn deserialize_int_from_number<'de, T, D>(deserializer: D) -> Result<T, D::Error>
where where
D: Deserializer<'de>, D: Deserializer<'de>,