mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 06:22:22 -04:00
use backend for genCards() and updateFieldCache()
This commit is contained in:
parent
05ca797ee6
commit
a7a485d550
10 changed files with 132 additions and 134 deletions
|
@ -92,6 +92,7 @@ message BackendInput {
|
|||
Empty deck_tree_legacy = 77;
|
||||
FieldNamesForNotesIn field_names_for_notes = 78;
|
||||
FindAndReplaceIn find_and_replace = 79;
|
||||
AfterNoteUpdatesIn after_note_updates = 80;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -163,6 +164,7 @@ message BackendOutput {
|
|||
bytes deck_tree_legacy = 77;
|
||||
FieldNamesForNotesOut field_names_for_notes = 78;
|
||||
uint32 find_and_replace = 79;
|
||||
Empty after_note_updates = 80;
|
||||
|
||||
BackendError error = 2047;
|
||||
}
|
||||
|
@ -732,3 +734,9 @@ message FindAndReplaceIn {
|
|||
bool match_case = 5;
|
||||
string field_name = 6;
|
||||
}
|
||||
|
||||
message AfterNoteUpdatesIn {
|
||||
repeated int64 nids = 1;
|
||||
bool mark_notes_modified = 2;
|
||||
bool generate_cards = 3;
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ import time
|
|||
import traceback
|
||||
import unicodedata
|
||||
import weakref
|
||||
from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, Union
|
||||
from typing import Any, Iterable, List, Optional, Sequence, Tuple, Union
|
||||
|
||||
import anki.find
|
||||
import anki.latex # sets up hook
|
||||
|
@ -34,16 +34,7 @@ from anki.rsbackend import TR, RustBackend
|
|||
from anki.sched import Scheduler as V1Scheduler
|
||||
from anki.schedv2 import Scheduler as V2Scheduler
|
||||
from anki.tags import TagManager
|
||||
from anki.utils import (
|
||||
devMode,
|
||||
fieldChecksum,
|
||||
ids2str,
|
||||
intTime,
|
||||
joinFields,
|
||||
maxID,
|
||||
splitFields,
|
||||
stripHTMLMedia,
|
||||
)
|
||||
from anki.utils import devMode, ids2str, intTime, joinFields
|
||||
|
||||
|
||||
# this is initialized by storage.Collection
|
||||
|
@ -359,81 +350,6 @@ crt=?, mod=?, scm=?, dty=?, usn=?, ls=?""",
|
|||
ok.append(t)
|
||||
return ok
|
||||
|
||||
def genCards(self, nids: List[int]) -> List[int]:
|
||||
"Generate cards for non-empty templates, return ids to remove."
|
||||
# build map of (nid,ord) so we don't create dupes
|
||||
snids = ids2str(nids)
|
||||
have: Dict[int, Dict[int, int]] = {}
|
||||
dids: Dict[int, Optional[int]] = {}
|
||||
dues: Dict[int, int] = {}
|
||||
for id, nid, ord, did, due, odue, odid, type in self.db.execute(
|
||||
"select id, nid, ord, did, due, odue, odid, type from cards where nid in "
|
||||
+ snids
|
||||
):
|
||||
# existing cards
|
||||
if nid not in have:
|
||||
have[nid] = {}
|
||||
have[nid][ord] = id
|
||||
# if in a filtered deck, add new cards to original deck
|
||||
if odid != 0:
|
||||
did = odid
|
||||
# and their dids
|
||||
if nid in dids:
|
||||
if dids[nid] and dids[nid] != did:
|
||||
# cards are in two or more different decks; revert to
|
||||
# model default
|
||||
dids[nid] = None
|
||||
else:
|
||||
# first card or multiple cards in same deck
|
||||
dids[nid] = did
|
||||
# save due
|
||||
if odid != 0:
|
||||
due = odue
|
||||
if nid not in dues and type == 0:
|
||||
# Add due to new card only if it's the due of a new sibling
|
||||
dues[nid] = due
|
||||
# build cards for each note
|
||||
data = []
|
||||
ts = maxID(self.db)
|
||||
now = intTime()
|
||||
rem = []
|
||||
usn = self.usn()
|
||||
for nid, mid, flds in self.db.execute(
|
||||
"select id, mid, flds from notes where id in " + snids
|
||||
):
|
||||
model = self.models.get(mid)
|
||||
assert model
|
||||
avail = self.models.availOrds(model, flds)
|
||||
did = dids.get(nid) or model["did"]
|
||||
due = dues.get(nid)
|
||||
# add any missing cards
|
||||
for t in self._tmplsFromOrds(model, avail):
|
||||
doHave = nid in have and t["ord"] in have[nid]
|
||||
if not doHave:
|
||||
# check deck is not a cram deck
|
||||
did = t["did"] or did
|
||||
if self.decks.isDyn(did):
|
||||
did = 1
|
||||
# if the deck doesn't exist, use default instead
|
||||
did = self.decks.get(did)["id"]
|
||||
# use sibling due# if there is one, else use a new id
|
||||
if due is None:
|
||||
due = self.nextID("pos")
|
||||
data.append((ts, nid, did, t["ord"], now, usn, due))
|
||||
ts += 1
|
||||
# note any cards that need removing
|
||||
if nid in have:
|
||||
for ord, id in list(have[nid].items()):
|
||||
if ord not in avail:
|
||||
rem.append(id)
|
||||
# bulk update
|
||||
self.db.executemany(
|
||||
"""
|
||||
insert into cards values (?,?,?,?,?,?,0,0,?,0,0,0,0,0,0,0,0,"")""",
|
||||
data,
|
||||
)
|
||||
return rem
|
||||
|
||||
# type is no longer used
|
||||
def previewCards(
|
||||
self, note: Note, type: int = 0, did: Optional[int] = None
|
||||
|
@ -535,31 +451,24 @@ select id from notes where id in %s and id not in (select nid from cards)"""
|
|||
print("emptyCids() will go away")
|
||||
return []
|
||||
|
||||
# Field checksums and sorting fields
|
||||
# Card generation & field checksums/sort fields
|
||||
##########################################################################
|
||||
|
||||
def _fieldData(self, snids: str) -> Any:
|
||||
return self.db.execute("select id, mid, flds from notes where id in " + snids)
|
||||
def after_note_updates(self, nids: List[int], mark_modified: bool, generate_cards: bool = True) -> None:
|
||||
self.backend.after_note_updates(
|
||||
nids=nids, generate_cards=generate_cards, mark_notes_modified=mark_modified
|
||||
)
|
||||
|
||||
# legacy
|
||||
|
||||
def updateFieldCache(self, nids: List[int]) -> None:
|
||||
"Update field checksums and sort cache, after find&replace, etc."
|
||||
snids = ids2str(nids)
|
||||
r = []
|
||||
for (nid, mid, flds) in self._fieldData(snids):
|
||||
fields = splitFields(flds)
|
||||
model = self.models.get(mid)
|
||||
if not model:
|
||||
# note points to invalid model
|
||||
continue
|
||||
r.append(
|
||||
(
|
||||
stripHTMLMedia(fields[self.models.sortIdx(model)]),
|
||||
fieldChecksum(fields[0]),
|
||||
nid,
|
||||
)
|
||||
)
|
||||
# apply, relying on calling code to bump usn+mod
|
||||
self.db.executemany("update notes set sfld=?, csum=? where id=?", r)
|
||||
self.after_note_updates(nids, mark_modified=False, generate_cards=False)
|
||||
|
||||
# this also updates field cache
|
||||
def genCards(self, nids: List[int]) -> List[int]:
|
||||
self.after_note_updates(nids, mark_modified=False, generate_cards=True)
|
||||
# previously returned empty cards, no longer does
|
||||
return []
|
||||
|
||||
# Finding cards
|
||||
##########################################################################
|
||||
|
@ -909,7 +818,7 @@ select id from cards where odid > 0 and did in %s"""
|
|||
self.tags.registerNotes()
|
||||
# field cache
|
||||
for m in self.models.all():
|
||||
self.updateFieldCache(self.models.nids(m))
|
||||
self.after_note_updates(self.models.nids(m), mark_modified=False, generate_cards=False)
|
||||
# new cards can't have a due position > 32 bits, so wrap items over
|
||||
# 2 million back to 1 million
|
||||
self.db.execute(
|
||||
|
|
|
@ -197,11 +197,8 @@ class NoteImporter(Importer):
|
|||
firsts[fld0] = True
|
||||
self.addNew(new)
|
||||
self.addUpdates(updates)
|
||||
# make sure to update sflds, etc
|
||||
self.col.updateFieldCache(self._ids)
|
||||
# generate cards
|
||||
if self.col.genCards(self._ids):
|
||||
self.log.insert(0, _("Empty cards found. Please run Tools>Empty Cards."))
|
||||
# generate cards + update field cache
|
||||
self.col.after_note_updates(self._ids, mark_modified=False)
|
||||
# apply scheduling updates
|
||||
self.updateCards()
|
||||
# we randomize or order here, to ensure that siblings
|
||||
|
|
|
@ -448,7 +448,7 @@ class ModelManager:
|
|||
self._changeNotes(nids, newModel, fmap)
|
||||
if cmap:
|
||||
self._changeCards(nids, m, newModel, cmap)
|
||||
self.col.genCards(nids)
|
||||
self.col.after_note_updates(nids, mark_modified=True)
|
||||
|
||||
def _changeNotes(
|
||||
self, nids: List[int], newModel: NoteType, map: Dict[int, Union[None, int]]
|
||||
|
@ -470,7 +470,6 @@ class ModelManager:
|
|||
self.col.db.executemany(
|
||||
"update notes set flds=?,mid=?,mod=?,usn=? where id = ?", d
|
||||
)
|
||||
self.col.updateFieldCache(nids)
|
||||
|
||||
def _changeCards(
|
||||
self,
|
||||
|
@ -518,7 +517,7 @@ class ModelManager:
|
|||
# Required field/text cache
|
||||
##########################################################################
|
||||
|
||||
# fixme: genCards(), clayout, importing, cards.isEmpty
|
||||
# fixme: clayout, importing, cards.isEmpty
|
||||
def availOrds(self, m: NoteType, flds: str) -> List:
|
||||
"Given a joined field string, return available template ordinals."
|
||||
if m["type"] == MODEL_CLOZE:
|
||||
|
|
|
@ -778,6 +778,20 @@ class RustBackend:
|
|||
release_gil=True,
|
||||
).find_and_replace
|
||||
|
||||
def after_note_updates(
|
||||
self, nids: List[int], generate_cards: bool, mark_notes_modified: bool
|
||||
) -> None:
|
||||
self._run_command(
|
||||
pb.BackendInput(
|
||||
after_note_updates=pb.AfterNoteUpdatesIn(
|
||||
nids=nids,
|
||||
generate_cards=generate_cards,
|
||||
mark_notes_modified=mark_notes_modified,
|
||||
)
|
||||
),
|
||||
release_gil=True
|
||||
)
|
||||
|
||||
|
||||
def translate_string_in(
|
||||
key: TR, **kwargs: Union[str, int, float]
|
||||
|
|
|
@ -366,6 +366,9 @@ impl Backend {
|
|||
OValue::FieldNamesForNotes(self.field_names_for_notes(input)?)
|
||||
}
|
||||
Value::FindAndReplace(input) => OValue::FindAndReplace(self.find_and_replace(input)?),
|
||||
Value::AfterNoteUpdates(input) => {
|
||||
OValue::AfterNoteUpdates(self.after_note_updates(input)?)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -1094,6 +1097,21 @@ impl Backend {
|
|||
col.find_and_replace(FindReplaceContext::new(nids, &search, &repl, field_name)?)
|
||||
})
|
||||
}
|
||||
|
||||
fn after_note_updates(&self, input: pb::AfterNoteUpdatesIn) -> Result<pb::Empty> {
|
||||
self.with_col(|col| {
|
||||
col.transact(None, |col| {
|
||||
let nids: Vec<_> = input.nids.into_iter().map(NoteID).collect();
|
||||
col.after_note_updates(
|
||||
&nids,
|
||||
col.usn()?,
|
||||
input.generate_cards,
|
||||
input.mark_notes_modified,
|
||||
)?;
|
||||
Ok(pb::Empty {})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn translate_arg_to_fluent_val(arg: &pb::TranslateArgValue) -> FluentValue {
|
||||
|
|
|
@ -77,7 +77,7 @@ impl Collection {
|
|||
}
|
||||
}
|
||||
if changed {
|
||||
self.update_note_inner(&genctx, &mut note)?;
|
||||
self.update_note_inner_generating_cards(&genctx, &mut note, true)?;
|
||||
total_changed += 1;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -404,7 +404,8 @@ where
|
|||
&self.mgr.media_folder,
|
||||
)? {
|
||||
// note was modified, needs saving
|
||||
note.prepare_for_update(nt, Some(usn))?;
|
||||
note.prepare_for_update(nt)?;
|
||||
note.set_modified(usn);
|
||||
self.ctx.storage.update_note(¬e)?;
|
||||
collection_modified = true;
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ use crate::{
|
|||
timestamp::TimestampSecs,
|
||||
types::Usn,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use num_integer::Integer;
|
||||
use std::{collections::HashSet, convert::TryInto};
|
||||
|
||||
|
@ -63,8 +64,8 @@ impl Note {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Prepare note for saving to the database. If usn is provided, mtime will be bumped.
|
||||
pub fn prepare_for_update(&mut self, nt: &NoteType, usn: Option<Usn>) -> Result<()> {
|
||||
/// Prepare note for saving to the database. Does not mark it as modified.
|
||||
pub fn prepare_for_update(&mut self, nt: &NoteType) -> Result<()> {
|
||||
assert!(nt.id == self.ntid);
|
||||
if nt.fields.len() != self.fields.len() {
|
||||
return Err(AnkiError::invalid_input(format!(
|
||||
|
@ -88,13 +89,14 @@ impl Note {
|
|||
};
|
||||
self.sort_field = Some(sort_field.into());
|
||||
self.checksum = Some(checksum);
|
||||
if let Some(usn) = usn {
|
||||
self.mtime = TimestampSecs::now();
|
||||
self.usn = usn;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn set_modified(&mut self, usn: Usn) {
|
||||
self.mtime = TimestampSecs::now();
|
||||
self.usn = usn;
|
||||
}
|
||||
|
||||
pub(crate) fn nonempty_fields<'a>(&self, fields: &'a [NoteField]) -> HashSet<&'a str> {
|
||||
self.fields
|
||||
.iter()
|
||||
|
@ -193,7 +195,8 @@ impl Collection {
|
|||
did: DeckID,
|
||||
) -> Result<()> {
|
||||
self.canonify_note_tags(note, ctx.usn)?;
|
||||
note.prepare_for_update(&ctx.notetype, Some(ctx.usn))?;
|
||||
note.prepare_for_update(&ctx.notetype)?;
|
||||
note.set_modified(ctx.usn);
|
||||
self.storage.add_note(note)?;
|
||||
self.generate_cards_for_new_note(ctx, note, did)
|
||||
}
|
||||
|
@ -204,21 +207,33 @@ impl Collection {
|
|||
.get_notetype(note.ntid)?
|
||||
.ok_or_else(|| AnkiError::invalid_input("missing note type"))?;
|
||||
let ctx = CardGenContext::new(&nt, col.usn()?);
|
||||
col.update_note_inner(&ctx, note)
|
||||
col.update_note_inner_generating_cards(&ctx, note, true)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn update_note_inner(
|
||||
pub(crate) fn update_note_inner_generating_cards(
|
||||
&mut self,
|
||||
ctx: &CardGenContext,
|
||||
note: &mut Note,
|
||||
mark_note_modified: bool,
|
||||
) -> Result<()> {
|
||||
self.canonify_note_tags(note, ctx.usn)?;
|
||||
note.prepare_for_update(ctx.notetype, Some(ctx.usn))?;
|
||||
self.generate_cards_for_existing_note(ctx, note)?;
|
||||
self.storage.update_note(note)?;
|
||||
self.update_note_inner_without_cards(note, ctx.notetype, ctx.usn, mark_note_modified)?;
|
||||
self.generate_cards_for_existing_note(ctx, note)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
pub(crate) fn update_note_inner_without_cards(
|
||||
&mut self,
|
||||
note: &mut Note,
|
||||
nt: &NoteType,
|
||||
usn: Usn,
|
||||
mark_note_modified: bool,
|
||||
) -> Result<()> {
|
||||
self.canonify_note_tags(note, usn)?;
|
||||
note.prepare_for_update(nt)?;
|
||||
if mark_note_modified {
|
||||
note.set_modified(usn);
|
||||
}
|
||||
self.storage.update_note(note)
|
||||
}
|
||||
|
||||
/// Remove a note. Cards must already have been deleted.
|
||||
|
@ -230,6 +245,42 @@ impl Collection {
|
|||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update cards and field cache after notes modified externally.
|
||||
/// If gencards is false, skip card generation.
|
||||
pub(crate) fn after_note_updates(
|
||||
&mut self,
|
||||
nids: &[NoteID],
|
||||
usn: Usn,
|
||||
generate_cards: bool,
|
||||
mark_notes_modified: bool,
|
||||
) -> Result<()> {
|
||||
let nids_by_notetype = self.storage.note_ids_by_notetype(nids)?;
|
||||
for (ntid, group) in &nids_by_notetype.into_iter().group_by(|tup| tup.0) {
|
||||
let nt = self
|
||||
.get_notetype(ntid)?
|
||||
.ok_or_else(|| AnkiError::invalid_input("missing note type"))?;
|
||||
let genctx = CardGenContext::new(&nt, usn);
|
||||
for (_, nid) in group {
|
||||
let mut note = self.storage.get_note(nid)?.unwrap();
|
||||
if generate_cards {
|
||||
self.update_note_inner_generating_cards(
|
||||
&genctx,
|
||||
&mut note,
|
||||
mark_notes_modified,
|
||||
)?;
|
||||
} else {
|
||||
self.update_note_inner_without_cards(
|
||||
&mut note,
|
||||
&genctx.notetype,
|
||||
usn,
|
||||
mark_notes_modified,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -63,7 +63,7 @@ impl Collection {
|
|||
let nids = self.search_notes_only(&format!("mid:{}", nt.id))?;
|
||||
for nid in nids {
|
||||
let mut note = self.storage.get_note(nid)?.unwrap();
|
||||
note.prepare_for_update(nt, None)?;
|
||||
note.prepare_for_update(nt)?;
|
||||
self.storage.update_note(¬e)?;
|
||||
}
|
||||
} else {
|
||||
|
@ -92,7 +92,8 @@ impl Collection {
|
|||
})
|
||||
.map(Into::into)
|
||||
.collect();
|
||||
note.prepare_for_update(nt, Some(usn))?;
|
||||
note.prepare_for_update(nt)?;
|
||||
note.set_modified(usn);
|
||||
self.storage.update_note(¬e)?;
|
||||
}
|
||||
Ok(())
|
||||
|
|
Loading…
Reference in a new issue