make flag changes in the reviewer undoable
This splits update_card() into separate undoable/non-undoable ops like the change to notes in b4396b94abdeba3347d30025c5c0240d991006c9 It means that actions get a blanket 'Update Card' description - in the future we'll probably want to either add specific actions to the backend, or allow an enum or string to be passed in to describe the op. Other changes: - card.flush() can no longer be used to add new cards. Card creation is only supposed to be done in response to changes in a note's fields, and this functionality was only exposed because the card generation hadn't been migrated to the backend at that point. As far as I'm aware, only Arthur's "copy notes" add-on used this functionality, and that should be an easy fix - when the new note is added, the associated cards will be generated, and they can then be retrieved with note.cards() - tidy ups/PEP8
This commit is contained in:
parent
d4765a301f
commit
3d0ddc8539
@ -14,3 +14,4 @@ undo-unbury-unsuspend = Unbury/Unsuspend
|
|||||||
undo-add-note = Add Note
|
undo-add-note = Add Note
|
||||||
undo-update-tag = Update Tag
|
undo-update-tag = Update Tag
|
||||||
undo-update-note = Update Note
|
undo-update-note = Update Note
|
||||||
|
undo-update-card = Update Card
|
||||||
|
@ -74,19 +74,9 @@ class Card:
|
|||||||
self.flags = c.flags
|
self.flags = c.flags
|
||||||
self.data = c.data
|
self.data = c.data
|
||||||
|
|
||||||
def _bugcheck(self) -> None:
|
def _to_backend_card(self) -> _pb.Card:
|
||||||
if (
|
|
||||||
self.queue == QUEUE_TYPE_REV
|
|
||||||
and self.odue
|
|
||||||
and not self.col.decks.isDyn(self.did)
|
|
||||||
):
|
|
||||||
hooks.card_odue_was_invalid()
|
|
||||||
|
|
||||||
def flush(self) -> None:
|
|
||||||
self._bugcheck()
|
|
||||||
hooks.card_will_flush(self)
|
|
||||||
# mtime & usn are set by backend
|
# mtime & usn are set by backend
|
||||||
card = _pb.Card(
|
return _pb.Card(
|
||||||
id=self.id,
|
id=self.id,
|
||||||
note_id=self.nid,
|
note_id=self.nid,
|
||||||
deck_id=self.did,
|
deck_id=self.did,
|
||||||
@ -104,10 +94,15 @@ class Card:
|
|||||||
flags=self.flags,
|
flags=self.flags,
|
||||||
data=self.data,
|
data=self.data,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def flush(self) -> None:
|
||||||
|
hooks.card_will_flush(self)
|
||||||
if self.id != 0:
|
if self.id != 0:
|
||||||
self.col._backend.update_card(card)
|
self.col._backend.update_card(
|
||||||
|
card=self._to_backend_card(), skip_undo_entry=True
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self.id = self.col._backend.add_card(card)
|
raise Exception("card.flush() expects an existing card")
|
||||||
|
|
||||||
def question(self, reload: bool = False, browser: bool = False) -> str:
|
def question(self, reload: bool = False, browser: bool = False) -> str:
|
||||||
return self.render_output(reload, browser).question_and_style()
|
return self.render_output(reload, browser).question_and_style()
|
||||||
@ -197,9 +192,14 @@ class Card:
|
|||||||
del d["timerStarted"]
|
del d["timerStarted"]
|
||||||
return f"{super().__repr__()} {pprint.pformat(d, width=300)}"
|
return f"{super().__repr__()} {pprint.pformat(d, width=300)}"
|
||||||
|
|
||||||
def userFlag(self) -> int:
|
def user_flag(self) -> int:
|
||||||
return self.flags & 0b111
|
return self.flags & 0b111
|
||||||
|
|
||||||
def setUserFlag(self, flag: int) -> None:
|
def set_user_flag(self, flag: int) -> None:
|
||||||
assert 0 <= flag <= 7
|
assert 0 <= flag <= 7
|
||||||
self.flags = (self.flags & ~0b111) | flag
|
self.flags = (self.flags & ~0b111) | flag
|
||||||
|
|
||||||
|
# legacy
|
||||||
|
|
||||||
|
userFlag = user_flag
|
||||||
|
setUserFlag = set_user_flag
|
||||||
|
@ -319,17 +319,23 @@ class Collection:
|
|||||||
# Object creation helpers
|
# Object creation helpers
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def getCard(self, id: int) -> Card:
|
def get_card(self, id: int) -> Card:
|
||||||
return Card(self, id)
|
return Card(self, id)
|
||||||
|
|
||||||
|
def update_card(self, card: Card) -> None:
|
||||||
|
"""Save card changes to database, and add an undo entry.
|
||||||
|
Unlike card.flush(), this will invalidate any current checkpoint."""
|
||||||
|
self._backend.update_card(card=card._to_backend_card(), skip_undo_entry=False)
|
||||||
|
|
||||||
def get_note(self, id: int) -> Note:
|
def get_note(self, id: int) -> Note:
|
||||||
return Note(self, id=id)
|
return Note(self, id=id)
|
||||||
|
|
||||||
def update_note(self, note: Note) -> None:
|
def update_note(self, note: Note) -> None:
|
||||||
"""Save note changes to database, and add an undo entry.
|
"""Save note changes to database, and add an undo entry.
|
||||||
Unlike note.flush(), this will invalidate any current checkpoint."""
|
Unlike note.flush(), this will invalidate any current checkpoint."""
|
||||||
self._backend.update_note(note=note.to_backend_note(), skip_undo_entry=False)
|
self._backend.update_note(note=note._to_backend_note(), skip_undo_entry=False)
|
||||||
|
|
||||||
|
getCard = get_card
|
||||||
getNote = get_note
|
getNote = get_note
|
||||||
|
|
||||||
# Utils
|
# Utils
|
||||||
@ -367,7 +373,7 @@ class Collection:
|
|||||||
return Note(self, self.models.current(forDeck))
|
return Note(self, self.models.current(forDeck))
|
||||||
|
|
||||||
def add_note(self, note: Note, deck_id: int) -> None:
|
def add_note(self, note: Note, deck_id: int) -> None:
|
||||||
note.id = self._backend.add_note(note=note.to_backend_note(), deck_id=deck_id)
|
note.id = self._backend.add_note(note=note._to_backend_note(), deck_id=deck_id)
|
||||||
|
|
||||||
def remove_notes(self, note_ids: Sequence[int]) -> None:
|
def remove_notes(self, note_ids: Sequence[int]) -> None:
|
||||||
hooks.notes_will_be_deleted(self, note_ids)
|
hooks.notes_will_be_deleted(self, note_ids)
|
||||||
@ -953,7 +959,7 @@ table.review-log {{ {revlog_style} }}
|
|||||||
# Card Flags
|
# Card Flags
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def setUserFlag(self, flag: int, cids: List[int]) -> None:
|
def set_user_flag_for_cards(self, flag: int, cids: List[int]) -> None:
|
||||||
assert 0 <= flag <= 7
|
assert 0 <= flag <= 7
|
||||||
self.db.execute(
|
self.db.execute(
|
||||||
"update cards set flags = (flags & ~?) | ?, usn=?, mod=? where id in %s"
|
"update cards set flags = (flags & ~?) | ?, usn=?, mod=? where id in %s"
|
||||||
|
@ -53,7 +53,7 @@ class Note:
|
|||||||
self.fields = list(n.fields)
|
self.fields = list(n.fields)
|
||||||
self._fmap = self.col.models.fieldMap(self.model())
|
self._fmap = self.col.models.fieldMap(self.model())
|
||||||
|
|
||||||
def to_backend_note(self) -> _pb.Note:
|
def _to_backend_note(self) -> _pb.Note:
|
||||||
hooks.note_will_flush(self)
|
hooks.note_will_flush(self)
|
||||||
return _pb.Note(
|
return _pb.Note(
|
||||||
id=self.id,
|
id=self.id,
|
||||||
@ -69,7 +69,9 @@ class Note:
|
|||||||
"""This preserves any current checkpoint.
|
"""This preserves any current checkpoint.
|
||||||
For an undo entry, use col.update_note() instead."""
|
For an undo entry, use col.update_note() instead."""
|
||||||
assert self.id != 0
|
assert self.id != 0
|
||||||
self.col._backend.update_note(note=self.to_backend_note(), skip_undo_entry=True)
|
self.col._backend.update_note(
|
||||||
|
note=self._to_backend_note(), skip_undo_entry=True
|
||||||
|
)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
d = dict(self.__dict__)
|
d = dict(self.__dict__)
|
||||||
@ -124,7 +126,7 @@ class Note:
|
|||||||
_model = property(model)
|
_model = property(model)
|
||||||
|
|
||||||
def cloze_numbers_in_fields(self) -> Sequence[int]:
|
def cloze_numbers_in_fields(self) -> Sequence[int]:
|
||||||
return self.col._backend.cloze_numbers_in_note(self.to_backend_note())
|
return self.col._backend.cloze_numbers_in_note(self._to_backend_note())
|
||||||
|
|
||||||
# Dict interface
|
# Dict interface
|
||||||
##################################################
|
##################################################
|
||||||
@ -187,5 +189,5 @@ class Note:
|
|||||||
def dupeOrEmpty(self) -> int:
|
def dupeOrEmpty(self) -> int:
|
||||||
"1 if first is empty; 2 if first is a duplicate, 0 otherwise."
|
"1 if first is empty; 2 if first is a duplicate, 0 otherwise."
|
||||||
return self.col._backend.note_is_duplicate_or_empty(
|
return self.col._backend.note_is_duplicate_or_empty(
|
||||||
self.to_backend_note()
|
self._to_backend_note()
|
||||||
).state
|
).state
|
||||||
|
@ -178,7 +178,7 @@ class TemplateRenderContext:
|
|||||||
fields["Card"] = self._template["name"]
|
fields["Card"] = self._template["name"]
|
||||||
else:
|
else:
|
||||||
fields["Card"] = ""
|
fields["Card"] = ""
|
||||||
flag = self._card.userFlag()
|
flag = self._card.user_flag()
|
||||||
fields["CardFlag"] = flag and f"flag{flag}" or ""
|
fields["CardFlag"] = flag and f"flag{flag}" or ""
|
||||||
self._fields = fields
|
self._fields = fields
|
||||||
|
|
||||||
@ -239,7 +239,7 @@ class TemplateRenderContext:
|
|||||||
if self._template:
|
if self._template:
|
||||||
# card layout screen
|
# card layout screen
|
||||||
out = self._col._backend.render_uncommitted_card(
|
out = self._col._backend.render_uncommitted_card(
|
||||||
note=self._note.to_backend_note(),
|
note=self._note._to_backend_note(),
|
||||||
card_ord=self._card.ord,
|
card_ord=self._card.ord,
|
||||||
template=to_json_bytes(self._template),
|
template=to_json_bytes(self._template),
|
||||||
fill_empty=self._fill_empty,
|
fill_empty=self._fill_empty,
|
||||||
|
@ -13,30 +13,30 @@ def test_flags():
|
|||||||
c.flags = origBits
|
c.flags = origBits
|
||||||
c.flush()
|
c.flush()
|
||||||
# no flags to start with
|
# no flags to start with
|
||||||
assert c.userFlag() == 0
|
assert c.user_flag() == 0
|
||||||
assert len(col.findCards("flag:0")) == 1
|
assert len(col.findCards("flag:0")) == 1
|
||||||
assert len(col.findCards("flag:1")) == 0
|
assert len(col.findCards("flag:1")) == 0
|
||||||
# set flag 2
|
# set flag 2
|
||||||
col.setUserFlag(2, [c.id])
|
col.set_user_flag_for_cards(2, [c.id])
|
||||||
c.load()
|
c.load()
|
||||||
assert c.userFlag() == 2
|
assert c.user_flag() == 2
|
||||||
assert c.flags & origBits == origBits
|
assert c.flags & origBits == origBits
|
||||||
assert len(col.findCards("flag:0")) == 0
|
assert len(col.findCards("flag:0")) == 0
|
||||||
assert len(col.findCards("flag:2")) == 1
|
assert len(col.findCards("flag:2")) == 1
|
||||||
assert len(col.findCards("flag:3")) == 0
|
assert len(col.findCards("flag:3")) == 0
|
||||||
# change to 3
|
# change to 3
|
||||||
col.setUserFlag(3, [c.id])
|
col.set_user_flag_for_cards(3, [c.id])
|
||||||
c.load()
|
c.load()
|
||||||
assert c.userFlag() == 3
|
assert c.user_flag() == 3
|
||||||
# unset
|
# unset
|
||||||
col.setUserFlag(0, [c.id])
|
col.set_user_flag_for_cards(0, [c.id])
|
||||||
c.load()
|
c.load()
|
||||||
assert c.userFlag() == 0
|
assert c.user_flag() == 0
|
||||||
|
|
||||||
# should work with Cards method as well
|
# should work with Cards method as well
|
||||||
c.setUserFlag(2)
|
c.set_user_flag(2)
|
||||||
assert c.userFlag() == 2
|
assert c.user_flag() == 2
|
||||||
c.setUserFlag(3)
|
c.set_user_flag(3)
|
||||||
assert c.userFlag() == 3
|
assert c.user_flag() == 3
|
||||||
c.setUserFlag(0)
|
c.set_user_flag(0)
|
||||||
assert c.userFlag() == 0
|
assert c.user_flag() == 0
|
||||||
|
@ -559,6 +559,8 @@ def test_suspend():
|
|||||||
|
|
||||||
def test_cram():
|
def test_cram():
|
||||||
col = getEmptyCol()
|
col = getEmptyCol()
|
||||||
|
opt = col.models.byName("Basic (and reversed card)")
|
||||||
|
col.models.setCurrent(opt)
|
||||||
note = col.newNote()
|
note = col.newNote()
|
||||||
note["Front"] = "one"
|
note["Front"] = "one"
|
||||||
col.addNote(note)
|
col.addNote(note)
|
||||||
@ -654,10 +656,9 @@ def test_cram():
|
|||||||
c.load()
|
c.load()
|
||||||
assert col.sched.answerButtons(c) == 4
|
assert col.sched.answerButtons(c) == 4
|
||||||
# add a sibling so we can test minSpace, etc
|
# add a sibling so we can test minSpace, etc
|
||||||
c.col = None
|
note["Back"] = "foo"
|
||||||
c2 = copy.deepcopy(c)
|
note.flush()
|
||||||
c2.col = c.col = col
|
c2 = note.cards()[1]
|
||||||
c2.id = 0
|
|
||||||
c2.ord = 1
|
c2.ord = 1
|
||||||
c2.due = 325
|
c2.due = 325
|
||||||
c2.flush()
|
c2.flush()
|
||||||
|
@ -411,8 +411,8 @@ class StatusDelegate(QItemDelegate):
|
|||||||
option.direction = Qt.RightToLeft
|
option.direction = Qt.RightToLeft
|
||||||
|
|
||||||
col = None
|
col = None
|
||||||
if c.userFlag() > 0:
|
if c.user_flag() > 0:
|
||||||
col = getattr(colors, f"FLAG{c.userFlag()}_BG")
|
col = getattr(colors, f"FLAG{c.user_flag()}_BG")
|
||||||
elif c.note().has_tag("Marked"):
|
elif c.note().has_tag("Marked"):
|
||||||
col = colors.MARKED_BG
|
col = colors.MARKED_BG
|
||||||
elif c.queue == QUEUE_TYPE_SUSPENDED:
|
elif c.queue == QUEUE_TYPE_SUSPENDED:
|
||||||
@ -1286,13 +1286,13 @@ where id in %s"""
|
|||||||
|
|
||||||
def _on_set_flag(self, n: int) -> None:
|
def _on_set_flag(self, n: int) -> None:
|
||||||
# flag needs toggling off?
|
# flag needs toggling off?
|
||||||
if n == self.card.userFlag():
|
if n == self.card.user_flag():
|
||||||
n = 0
|
n = 0
|
||||||
self.col.setUserFlag(n, self.selectedCards())
|
self.col.set_user_flag_for_cards(n, self.selectedCards())
|
||||||
self.model.reset()
|
self.model.reset()
|
||||||
|
|
||||||
def _updateFlagsMenu(self) -> None:
|
def _updateFlagsMenu(self) -> None:
|
||||||
flag = self.card and self.card.userFlag()
|
flag = self.card and self.card.user_flag()
|
||||||
flag = flag or 0
|
flag = flag or 0
|
||||||
|
|
||||||
f = self.form
|
f = self.form
|
||||||
|
@ -216,7 +216,7 @@ class Reviewer:
|
|||||||
return card.autoplay()
|
return card.autoplay()
|
||||||
|
|
||||||
def _update_flag_icon(self) -> None:
|
def _update_flag_icon(self) -> None:
|
||||||
self.web.eval(f"_drawFlag({self.card.userFlag()});")
|
self.web.eval(f"_drawFlag({self.card.user_flag()});")
|
||||||
|
|
||||||
def _update_mark_icon(self) -> None:
|
def _update_mark_icon(self) -> None:
|
||||||
self.web.eval(f"_drawMark({json.dumps(self.card.note().has_tag('marked'))});")
|
self.web.eval(f"_drawMark({json.dumps(self.card.note().has_tag('marked'))});")
|
||||||
@ -290,10 +290,10 @@ class Reviewer:
|
|||||||
("m", self.showContextMenu),
|
("m", self.showContextMenu),
|
||||||
("r", self.replayAudio),
|
("r", self.replayAudio),
|
||||||
(Qt.Key_F5, self.replayAudio),
|
(Qt.Key_F5, self.replayAudio),
|
||||||
("Ctrl+1", lambda: self.setFlag(1)),
|
("Ctrl+1", lambda: self.set_flag_on_current_card(1)),
|
||||||
("Ctrl+2", lambda: self.setFlag(2)),
|
("Ctrl+2", lambda: self.set_flag_on_current_card(2)),
|
||||||
("Ctrl+3", lambda: self.setFlag(3)),
|
("Ctrl+3", lambda: self.set_flag_on_current_card(3)),
|
||||||
("Ctrl+4", lambda: self.setFlag(4)),
|
("Ctrl+4", lambda: self.set_flag_on_current_card(4)),
|
||||||
("*", self.toggle_mark_on_current_note),
|
("*", self.toggle_mark_on_current_note),
|
||||||
("=", self.bury_current_note),
|
("=", self.bury_current_note),
|
||||||
("-", self.bury_current_card),
|
("-", self.bury_current_card),
|
||||||
@ -698,7 +698,7 @@ time = %(time)d;
|
|||||||
|
|
||||||
# note the shortcuts listed here also need to be defined above
|
# note the shortcuts listed here also need to be defined above
|
||||||
def _contextMenu(self) -> List[Any]:
|
def _contextMenu(self) -> List[Any]:
|
||||||
currentFlag = self.card and self.card.userFlag()
|
currentFlag = self.card and self.card.user_flag()
|
||||||
opts = [
|
opts = [
|
||||||
[
|
[
|
||||||
tr(TR.STUDYING_FLAG_CARD),
|
tr(TR.STUDYING_FLAG_CARD),
|
||||||
@ -706,25 +706,25 @@ time = %(time)d;
|
|||||||
[
|
[
|
||||||
tr(TR.ACTIONS_RED_FLAG),
|
tr(TR.ACTIONS_RED_FLAG),
|
||||||
"Ctrl+1",
|
"Ctrl+1",
|
||||||
lambda: self.setFlag(1),
|
lambda: self.set_flag_on_current_card(1),
|
||||||
dict(checked=currentFlag == 1),
|
dict(checked=currentFlag == 1),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
tr(TR.ACTIONS_ORANGE_FLAG),
|
tr(TR.ACTIONS_ORANGE_FLAG),
|
||||||
"Ctrl+2",
|
"Ctrl+2",
|
||||||
lambda: self.setFlag(2),
|
lambda: self.set_flag_on_current_card(2),
|
||||||
dict(checked=currentFlag == 2),
|
dict(checked=currentFlag == 2),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
tr(TR.ACTIONS_GREEN_FLAG),
|
tr(TR.ACTIONS_GREEN_FLAG),
|
||||||
"Ctrl+3",
|
"Ctrl+3",
|
||||||
lambda: self.setFlag(3),
|
lambda: self.set_flag_on_current_card(3),
|
||||||
dict(checked=currentFlag == 3),
|
dict(checked=currentFlag == 3),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
tr(TR.ACTIONS_BLUE_FLAG),
|
tr(TR.ACTIONS_BLUE_FLAG),
|
||||||
"Ctrl+4",
|
"Ctrl+4",
|
||||||
lambda: self.setFlag(4),
|
lambda: self.set_flag_on_current_card(4),
|
||||||
dict(checked=currentFlag == 4),
|
dict(checked=currentFlag == 4),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@ -782,12 +782,13 @@ time = %(time)d;
|
|||||||
def onOptions(self) -> None:
|
def onOptions(self) -> None:
|
||||||
self.mw.onDeckConf(self.mw.col.decks.get(self.card.odid or self.card.did))
|
self.mw.onDeckConf(self.mw.col.decks.get(self.card.odid or self.card.did))
|
||||||
|
|
||||||
def setFlag(self, flag: int) -> None:
|
def set_flag_on_current_card(self, flag: int) -> None:
|
||||||
# need to toggle off?
|
# need to toggle off?
|
||||||
if self.card.userFlag() == flag:
|
if self.card.user_flag() == flag:
|
||||||
flag = 0
|
flag = 0
|
||||||
self.card.setUserFlag(flag)
|
self.card.set_user_flag(flag)
|
||||||
self.card.flush()
|
self.mw.col.update_card(self.card)
|
||||||
|
self.mw.update_undo_actions()
|
||||||
self._update_flag_icon()
|
self._update_flag_icon()
|
||||||
|
|
||||||
def toggle_mark_on_current_note(self) -> None:
|
def toggle_mark_on_current_note(self) -> None:
|
||||||
@ -797,8 +798,8 @@ time = %(time)d;
|
|||||||
else:
|
else:
|
||||||
note.add_tag("marked")
|
note.add_tag("marked")
|
||||||
self.mw.col.update_note(note)
|
self.mw.col.update_note(note)
|
||||||
self._update_mark_icon()
|
|
||||||
self.mw.update_undo_actions()
|
self.mw.update_undo_actions()
|
||||||
|
self._update_mark_icon()
|
||||||
|
|
||||||
def on_set_due(self) -> None:
|
def on_set_due(self) -> None:
|
||||||
if self.mw.state != "review" or not self.card:
|
if self.mw.state != "review" or not self.card:
|
||||||
@ -863,3 +864,4 @@ time = %(time)d;
|
|||||||
onSuspendCard = suspend_current_card
|
onSuspendCard = suspend_current_card
|
||||||
onDelete = delete_current_note
|
onDelete = delete_current_note
|
||||||
onMark = toggle_mark_on_current_note
|
onMark = toggle_mark_on_current_note
|
||||||
|
setFlag = set_flag_on_current_card
|
||||||
|
@ -162,8 +162,7 @@ service BackendService {
|
|||||||
// cards
|
// cards
|
||||||
|
|
||||||
rpc GetCard(CardID) returns (Card);
|
rpc GetCard(CardID) returns (Card);
|
||||||
rpc UpdateCard(Card) returns (Empty);
|
rpc UpdateCard(UpdateCardIn) returns (Empty);
|
||||||
rpc AddCard(Card) returns (CardID);
|
|
||||||
rpc RemoveCards(RemoveCardsIn) returns (Empty);
|
rpc RemoveCards(RemoveCardsIn) returns (Empty);
|
||||||
rpc SetDeck(SetDeckIn) returns (Empty);
|
rpc SetDeck(SetDeckIn) returns (Empty);
|
||||||
|
|
||||||
@ -953,6 +952,11 @@ message UpdateNoteIn {
|
|||||||
bool skip_undo_entry = 2;
|
bool skip_undo_entry = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message UpdateCardIn {
|
||||||
|
Card card = 1;
|
||||||
|
bool skip_undo_entry = 2;
|
||||||
|
}
|
||||||
|
|
||||||
message EmptyCardsReport {
|
message EmptyCardsReport {
|
||||||
message NoteWithEmptyCards {
|
message NoteWithEmptyCards {
|
||||||
int64 note_id = 1;
|
int64 note_id = 1;
|
||||||
|
41
rslib/src/backend/card.rs
Normal file
41
rslib/src/backend/card.rs
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
use crate::prelude::*;
|
||||||
|
use crate::{
|
||||||
|
backend_proto as pb,
|
||||||
|
card::{CardQueue, CardType},
|
||||||
|
};
|
||||||
|
|
||||||
|
impl TryFrom<pb::Card> for Card {
|
||||||
|
type Error = AnkiError;
|
||||||
|
|
||||||
|
fn try_from(c: pb::Card) -> Result<Self, Self::Error> {
|
||||||
|
let ctype = CardType::try_from(c.ctype as u8)
|
||||||
|
.map_err(|_| AnkiError::invalid_input("invalid card type"))?;
|
||||||
|
let queue = CardQueue::try_from(c.queue as i8)
|
||||||
|
.map_err(|_| AnkiError::invalid_input("invalid card queue"))?;
|
||||||
|
Ok(Card {
|
||||||
|
id: CardID(c.id),
|
||||||
|
note_id: NoteID(c.note_id),
|
||||||
|
deck_id: DeckID(c.deck_id),
|
||||||
|
template_idx: c.template_idx as u16,
|
||||||
|
mtime: TimestampSecs(c.mtime_secs),
|
||||||
|
usn: Usn(c.usn),
|
||||||
|
ctype,
|
||||||
|
queue,
|
||||||
|
due: c.due,
|
||||||
|
interval: c.interval,
|
||||||
|
ease_factor: c.ease_factor as u16,
|
||||||
|
reps: c.reps,
|
||||||
|
lapses: c.lapses,
|
||||||
|
remaining_steps: c.remaining_steps,
|
||||||
|
original_due: c.original_due,
|
||||||
|
original_deck_id: DeckID(c.original_deck_id),
|
||||||
|
flags: c.flags as u8,
|
||||||
|
data: c.data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,12 @@
|
|||||||
// 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
|
||||||
|
|
||||||
|
mod card;
|
||||||
|
mod dbproxy;
|
||||||
|
mod generic;
|
||||||
|
mod http_sync_server;
|
||||||
|
mod scheduler;
|
||||||
|
|
||||||
pub use crate::backend_proto::BackendMethod;
|
pub use crate::backend_proto::BackendMethod;
|
||||||
use crate::{
|
use crate::{
|
||||||
backend::dbproxy::db_command_bytes,
|
backend::dbproxy::db_command_bytes,
|
||||||
@ -10,7 +16,6 @@ use crate::{
|
|||||||
AddOrUpdateDeckConfigLegacyIn, BackendResult, Empty, RenderedTemplateReplacement,
|
AddOrUpdateDeckConfigLegacyIn, BackendResult, Empty, RenderedTemplateReplacement,
|
||||||
},
|
},
|
||||||
card::{Card, CardID},
|
card::{Card, CardID},
|
||||||
card::{CardQueue, CardType},
|
|
||||||
cloze::add_cloze_numbers_in_string,
|
cloze::add_cloze_numbers_in_string,
|
||||||
collection::{open_collection, Collection},
|
collection::{open_collection, Collection},
|
||||||
config::SortKind,
|
config::SortKind,
|
||||||
@ -49,7 +54,6 @@ use crate::{
|
|||||||
template::RenderedNode,
|
template::RenderedNode,
|
||||||
text::{escape_anki_wildcards, extract_av_tags, sanitize_html_no_images, strip_av_tags, AVTag},
|
text::{escape_anki_wildcards, extract_av_tags, sanitize_html_no_images, strip_av_tags, AVTag},
|
||||||
timestamp::TimestampSecs,
|
timestamp::TimestampSecs,
|
||||||
types::Usn,
|
|
||||||
undo::UndoableOpKind,
|
undo::UndoableOpKind,
|
||||||
};
|
};
|
||||||
use fluent::FluentValue;
|
use fluent::FluentValue;
|
||||||
@ -69,11 +73,6 @@ use std::{
|
|||||||
};
|
};
|
||||||
use tokio::runtime::{self, Runtime};
|
use tokio::runtime::{self, Runtime};
|
||||||
|
|
||||||
mod dbproxy;
|
|
||||||
mod generic;
|
|
||||||
mod http_sync_server;
|
|
||||||
mod scheduler;
|
|
||||||
|
|
||||||
struct ThrottlingProgressHandler {
|
struct ThrottlingProgressHandler {
|
||||||
state: Arc<Mutex<ProgressState>>,
|
state: Arc<Mutex<ProgressState>>,
|
||||||
last_update: coarsetime::Instant,
|
last_update: coarsetime::Instant,
|
||||||
@ -985,26 +984,19 @@ impl BackendService for Backend {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_card(&self, input: pb::Card) -> BackendResult<Empty> {
|
fn update_card(&self, input: pb::UpdateCardIn) -> BackendResult<Empty> {
|
||||||
let mut card = pbcard_to_native(input)?;
|
|
||||||
self.with_col(|col| {
|
self.with_col(|col| {
|
||||||
col.transact(None, |ctx| {
|
let op = if input.skip_undo_entry {
|
||||||
let orig = ctx
|
None
|
||||||
.storage
|
} else {
|
||||||
.get_card(card.id)?
|
Some(UndoableOpKind::UpdateCard)
|
||||||
.ok_or_else(|| AnkiError::invalid_input("missing card"))?;
|
};
|
||||||
ctx.update_card(&mut card, &orig, ctx.usn()?)
|
let mut card: Card = input.card.ok_or(AnkiError::NotFound)?.try_into()?;
|
||||||
})
|
col.update_card_with_op(&mut card, op)
|
||||||
})
|
})
|
||||||
.map(Into::into)
|
.map(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_card(&self, input: pb::Card) -> BackendResult<pb::CardId> {
|
|
||||||
let mut card = pbcard_to_native(input)?;
|
|
||||||
self.with_col(|col| col.transact(None, |ctx| ctx.add_card(&mut card)))?;
|
|
||||||
Ok(pb::CardId { cid: card.id.0 })
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove_cards(&self, input: pb::RemoveCardsIn) -> BackendResult<Empty> {
|
fn remove_cards(&self, input: pb::RemoveCardsIn) -> BackendResult<Empty> {
|
||||||
self.with_col(|col| {
|
self.with_col(|col| {
|
||||||
col.transact(None, |col| {
|
col.transact(None, |col| {
|
||||||
@ -2063,33 +2055,6 @@ impl From<Card> for pb::Card {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pbcard_to_native(c: pb::Card) -> Result<Card> {
|
|
||||||
let ctype = CardType::try_from(c.ctype as u8)
|
|
||||||
.map_err(|_| AnkiError::invalid_input("invalid card type"))?;
|
|
||||||
let queue = CardQueue::try_from(c.queue as i8)
|
|
||||||
.map_err(|_| AnkiError::invalid_input("invalid card queue"))?;
|
|
||||||
Ok(Card {
|
|
||||||
id: CardID(c.id),
|
|
||||||
note_id: NoteID(c.note_id),
|
|
||||||
deck_id: DeckID(c.deck_id),
|
|
||||||
template_idx: c.template_idx as u16,
|
|
||||||
mtime: TimestampSecs(c.mtime_secs),
|
|
||||||
usn: Usn(c.usn),
|
|
||||||
ctype,
|
|
||||||
queue,
|
|
||||||
due: c.due,
|
|
||||||
interval: c.interval,
|
|
||||||
ease_factor: c.ease_factor as u16,
|
|
||||||
reps: c.reps,
|
|
||||||
lapses: c.lapses,
|
|
||||||
remaining_steps: c.remaining_steps,
|
|
||||||
original_due: c.original_due,
|
|
||||||
original_deck_id: DeckID(c.original_deck_id),
|
|
||||||
flags: c.flags as u8,
|
|
||||||
data: c.data,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<crate::scheduler::timing::SchedTimingToday> for pb::SchedTimingTodayOut {
|
impl From<crate::scheduler::timing::SchedTimingToday> for pb::SchedTimingTodayOut {
|
||||||
fn from(t: crate::scheduler::timing::SchedTimingToday) -> pb::SchedTimingTodayOut {
|
fn from(t: crate::scheduler::timing::SchedTimingToday) -> pb::SchedTimingTodayOut {
|
||||||
pb::SchedTimingTodayOut {
|
pb::SchedTimingTodayOut {
|
||||||
|
@ -3,12 +3,12 @@
|
|||||||
|
|
||||||
pub(crate) mod undo;
|
pub(crate) mod undo;
|
||||||
|
|
||||||
use crate::define_newtype;
|
|
||||||
use crate::err::{AnkiError, Result};
|
use crate::err::{AnkiError, Result};
|
||||||
use crate::notes::NoteID;
|
use crate::notes::NoteID;
|
||||||
use crate::{
|
use crate::{
|
||||||
collection::Collection, config::SchedulerVersion, timestamp::TimestampSecs, types::Usn,
|
collection::Collection, config::SchedulerVersion, timestamp::TimestampSecs, types::Usn,
|
||||||
};
|
};
|
||||||
|
use crate::{define_newtype, undo::UndoableOpKind};
|
||||||
|
|
||||||
use crate::{deckconf::DeckConf, decks::DeckID};
|
use crate::{deckconf::DeckConf, decks::DeckID};
|
||||||
use num_enum::TryFromPrimitive;
|
use num_enum::TryFromPrimitive;
|
||||||
@ -139,6 +139,15 @@ impl Card {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Collection {
|
impl Collection {
|
||||||
|
pub(crate) fn update_card_with_op(
|
||||||
|
&mut self,
|
||||||
|
card: &mut Card,
|
||||||
|
op: Option<UndoableOpKind>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let existing = self.storage.get_card(card.id)?.ok_or(AnkiError::NotFound)?;
|
||||||
|
self.transact(op, |col| col.update_card_inner(card, &existing, col.usn()?))
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) fn get_and_update_card<F, T>(&mut self, cid: CardID, func: F) -> Result<Card>
|
pub(crate) fn get_and_update_card<F, T>(&mut self, cid: CardID, func: F) -> Result<Card>
|
||||||
where
|
where
|
||||||
@ -150,12 +159,17 @@ impl Collection {
|
|||||||
.ok_or_else(|| AnkiError::invalid_input("no such card"))?;
|
.ok_or_else(|| AnkiError::invalid_input("no such card"))?;
|
||||||
let mut card = orig.clone();
|
let mut card = orig.clone();
|
||||||
func(&mut card)?;
|
func(&mut card)?;
|
||||||
self.update_card(&mut card, &orig, self.usn()?)?;
|
self.update_card_inner(&mut card, &orig, self.usn()?)?;
|
||||||
Ok(card)
|
Ok(card)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Marks the card as modified, then saves it.
|
/// Marks the card as modified, then saves it.
|
||||||
pub(crate) fn update_card(&mut self, card: &mut Card, original: &Card, usn: Usn) -> Result<()> {
|
pub(crate) fn update_card_inner(
|
||||||
|
&mut self,
|
||||||
|
card: &mut Card,
|
||||||
|
original: &Card,
|
||||||
|
usn: Usn,
|
||||||
|
) -> Result<()> {
|
||||||
card.set_modified(usn);
|
card.set_modified(usn);
|
||||||
self.update_card_undoable(card, original)
|
self.update_card_undoable(card, original)
|
||||||
}
|
}
|
||||||
@ -204,7 +218,7 @@ impl Collection {
|
|||||||
}
|
}
|
||||||
let original = card.clone();
|
let original = card.clone();
|
||||||
card.set_deck(deck_id, sched);
|
card.set_deck(deck_id, sched);
|
||||||
col.update_card(&mut card, &original, usn)?;
|
col.update_card_inner(&mut card, &original, usn)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
|
@ -184,7 +184,7 @@ impl Collection {
|
|||||||
if let Some(mut card) = self.storage.get_card(*cid)? {
|
if let Some(mut card) = self.storage.get_card(*cid)? {
|
||||||
let original = card.clone();
|
let original = card.clone();
|
||||||
card.remove_from_filtered_deck_restoring_queue(sched);
|
card.remove_from_filtered_deck_restoring_queue(sched);
|
||||||
self.update_card(&mut card, &original, usn)?;
|
self.update_card_inner(&mut card, &original, usn)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -249,7 +249,7 @@ impl Collection {
|
|||||||
for mut card in self.storage.all_searched_cards_in_search_order()? {
|
for mut card in self.storage.all_searched_cards_in_search_order()? {
|
||||||
let original = card.clone();
|
let original = card.clone();
|
||||||
card.move_into_filtered_deck(ctx, position);
|
card.move_into_filtered_deck(ctx, position);
|
||||||
self.update_card(&mut card, &original, ctx.usn)?;
|
self.update_card_inner(&mut card, &original, ctx.usn)?;
|
||||||
position += 1;
|
position += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -324,7 +324,8 @@ impl Collection {
|
|||||||
self.generate_cards_for_new_note(ctx, note, did)
|
self.generate_cards_for_new_note(ctx, note, did)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_note(&mut self, note: &mut Note) -> Result<()> {
|
#[cfg(test)]
|
||||||
|
pub(crate) fn update_note(&mut self, note: &mut Note) -> Result<()> {
|
||||||
self.update_note_with_op(note, Some(UndoableOpKind::UpdateNote))
|
self.update_note_with_op(note, Some(UndoableOpKind::UpdateNote))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -269,7 +269,7 @@ impl Collection {
|
|||||||
self.maybe_bury_siblings(&original, &updater.config)?;
|
self.maybe_bury_siblings(&original, &updater.config)?;
|
||||||
let timing = updater.timing;
|
let timing = updater.timing;
|
||||||
let mut card = updater.into_card();
|
let mut card = updater.into_card();
|
||||||
self.update_card(&mut card, &original, usn)?;
|
self.update_card_inner(&mut card, &original, usn)?;
|
||||||
if answer.new_state.leeched() {
|
if answer.new_state.leeched() {
|
||||||
self.add_leech_tag(card.note_id)?;
|
self.add_leech_tag(card.note_id)?;
|
||||||
}
|
}
|
||||||
|
@ -62,7 +62,7 @@ impl Collection {
|
|||||||
for original in self.storage.all_searched_cards()? {
|
for original in self.storage.all_searched_cards()? {
|
||||||
let mut card = original.clone();
|
let mut card = original.clone();
|
||||||
if card.restore_queue_after_bury_or_suspend() {
|
if card.restore_queue_after_bury_or_suspend() {
|
||||||
self.update_card(&mut card, &original, usn)?;
|
self.update_card_inner(&mut card, &original, usn)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.storage.clear_searched_cards_table()
|
self.storage.clear_searched_cards_table()
|
||||||
@ -113,7 +113,7 @@ impl Collection {
|
|||||||
card.remove_from_learning();
|
card.remove_from_learning();
|
||||||
}
|
}
|
||||||
card.queue = desired_queue;
|
card.queue = desired_queue;
|
||||||
self.update_card(&mut card, &original, usn)?;
|
self.update_card_inner(&mut card, &original, usn)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,7 +114,7 @@ impl Collection {
|
|||||||
if log {
|
if log {
|
||||||
col.log_manually_scheduled_review(&card, &original, usn)?;
|
col.log_manually_scheduled_review(&card, &original, usn)?;
|
||||||
}
|
}
|
||||||
col.update_card(&mut card, &original, usn)?;
|
col.update_card_inner(&mut card, &original, usn)?;
|
||||||
position += 1;
|
position += 1;
|
||||||
}
|
}
|
||||||
col.set_next_card_position(position)?;
|
col.set_next_card_position(position)?;
|
||||||
@ -155,7 +155,7 @@ impl Collection {
|
|||||||
for mut card in cards {
|
for mut card in cards {
|
||||||
let original = card.clone();
|
let original = card.clone();
|
||||||
card.set_new_position(sorter.position(&card));
|
card.set_new_position(sorter.position(&card));
|
||||||
self.update_card(&mut card, &original, usn)?;
|
self.update_card_inner(&mut card, &original, usn)?;
|
||||||
}
|
}
|
||||||
self.storage.clear_searched_cards_table()
|
self.storage.clear_searched_cards_table()
|
||||||
}
|
}
|
||||||
@ -177,7 +177,7 @@ impl Collection {
|
|||||||
for mut card in self.storage.all_searched_cards()? {
|
for mut card in self.storage.all_searched_cards()? {
|
||||||
let original = card.clone();
|
let original = card.clone();
|
||||||
card.set_new_position(card.due as u32 + by);
|
card.set_new_position(card.due as u32 + by);
|
||||||
self.update_card(&mut card, &original, usn)?;
|
self.update_card_inner(&mut card, &original, usn)?;
|
||||||
}
|
}
|
||||||
self.storage.clear_searched_cards_table()?;
|
self.storage.clear_searched_cards_table()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -96,7 +96,7 @@ impl Collection {
|
|||||||
let days_from_today = distribution.sample(&mut rng);
|
let days_from_today = distribution.sample(&mut rng);
|
||||||
card.set_due_date(today, days_from_today, spec.force_reset);
|
card.set_due_date(today, days_from_today, spec.force_reset);
|
||||||
col.log_manually_scheduled_review(&card, &original, usn)?;
|
col.log_manually_scheduled_review(&card, &original, usn)?;
|
||||||
col.update_card(&mut card, &original, usn)?;
|
col.update_card_inner(&mut card, &original, usn)?;
|
||||||
}
|
}
|
||||||
col.storage.clear_searched_cards_table()?;
|
col.storage.clear_searched_cards_table()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -25,7 +25,7 @@ impl UndoableOpKind {
|
|||||||
impl Collection {
|
impl Collection {
|
||||||
pub fn describe_op_kind(&self, op: UndoableOpKind) -> String {
|
pub fn describe_op_kind(&self, op: UndoableOpKind) -> String {
|
||||||
let key = match op {
|
let key = match op {
|
||||||
UndoableOpKind::UpdateCard => todo!(),
|
UndoableOpKind::UpdateCard => TR::UndoUpdateCard,
|
||||||
UndoableOpKind::AnswerCard => TR::UndoAnswerCard,
|
UndoableOpKind::AnswerCard => TR::UndoAnswerCard,
|
||||||
UndoableOpKind::Bury => TR::StudyingBury,
|
UndoableOpKind::Bury => TR::StudyingBury,
|
||||||
UndoableOpKind::Suspend => TR::StudyingSuspend,
|
UndoableOpKind::Suspend => TR::StudyingSuspend,
|
||||||
|
Loading…
Reference in New Issue
Block a user