mirror of
https://github.com/ankitects/anki.git
synced 2025-09-24 16:56:36 -04:00
handle default deck and filtered deck suppression in the backend
This commit is contained in:
parent
769bf04f75
commit
964a69e54e
15 changed files with 131 additions and 138 deletions
|
@ -84,7 +84,7 @@ message BackendInput {
|
|||
Empty get_empty_cards = 70;
|
||||
int64 get_deck_legacy = 71;
|
||||
string get_deck_id_by_name = 72;
|
||||
Empty get_deck_names = 73;
|
||||
GetDeckNamesIn get_deck_names = 73;
|
||||
AddOrUpdateDeckLegacyIn add_or_update_deck_legacy = 74;
|
||||
bool new_deck_legacy = 75;
|
||||
int64 remove_deck = 76;
|
||||
|
@ -784,3 +784,9 @@ message Preferences {
|
|||
message ClozeNumbersInNoteOut {
|
||||
repeated uint32 numbers = 1;
|
||||
}
|
||||
|
||||
message GetDeckNamesIn {
|
||||
bool skip_empty_default = 1;
|
||||
// if unset, implies skip_empty_default
|
||||
bool include_filtered = 2;
|
||||
}
|
||||
|
|
|
@ -4,8 +4,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import unicodedata
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
|
||||
|
||||
import anki # pylint: disable=unused-import
|
||||
import anki.backend_pb2 as pb
|
||||
|
@ -186,17 +185,13 @@ class DeckManager:
|
|||
if did in self.active():
|
||||
self.select(self.all_names_and_ids()[0].id)
|
||||
|
||||
def allNames(self, dyn: bool = True, force_default: bool = True) -> List:
|
||||
"An unsorted list of all deck names."
|
||||
if dyn:
|
||||
return [x["name"] for x in self.all(force_default=force_default)]
|
||||
else:
|
||||
return [
|
||||
x["name"] for x in self.all(force_default=force_default) if not x["dyn"]
|
||||
]
|
||||
|
||||
def all_names_and_ids(self) -> List[pb.DeckNameID]:
|
||||
return self.col.backend.get_deck_names_and_ids()
|
||||
def all_names_and_ids(
|
||||
self, skip_empty_default=False, include_filtered=True
|
||||
) -> Sequence[pb.DeckNameID]:
|
||||
"A sorted sequence of deck names and IDs."
|
||||
return self.col.backend.get_deck_names_and_ids(
|
||||
skip_empty_default, include_filtered
|
||||
)
|
||||
|
||||
def id_for_name(self, name: str) -> Optional[int]:
|
||||
return self.col.backend.get_deck_id_by_name(name)
|
||||
|
@ -221,29 +216,28 @@ class DeckManager:
|
|||
def deck_tree(self) -> pb.DeckTreeNode:
|
||||
return self.col.backend.deck_tree(include_counts=False)
|
||||
|
||||
def all(self, force_default: bool = True) -> List:
|
||||
"""A list of all decks.
|
||||
|
||||
list contains default deck if either:
|
||||
* force_default is True
|
||||
* there are no other deck
|
||||
* default deck contains a card
|
||||
* default deck has a child (assumed not to be the case if assume_no_child)
|
||||
"""
|
||||
decks = self.get_all_legacy()
|
||||
if not force_default and not self.should_default_be_displayed(force_default):
|
||||
decks = [deck for deck in decks if deck["id"] != 1]
|
||||
return decks
|
||||
def all(self) -> List:
|
||||
"All decks. Expensive; prefer all_names_and_ids()"
|
||||
return self.get_all_legacy()
|
||||
|
||||
def allIds(self) -> List[str]:
|
||||
print("decks.allIds() is deprecated, use .all_names_and_ids()")
|
||||
return [str(x.id) for x in self.all_names_and_ids()]
|
||||
|
||||
def allNames(self, dyn: bool = True, force_default: bool = True) -> List:
|
||||
print("decks.allNames() is deprecated, use .all_names_and_ids()")
|
||||
return [
|
||||
x.name
|
||||
for x in self.all_names_and_ids(
|
||||
skip_empty_default=not force_default, include_filtered=dyn
|
||||
)
|
||||
]
|
||||
|
||||
def collapse(self, did) -> None:
|
||||
deck = self.get(did)
|
||||
deck["collapsed"] = not deck["collapsed"]
|
||||
self.save(deck)
|
||||
|
||||
# fixme
|
||||
def collapseBrowser(self, did) -> None:
|
||||
deck = self.get(did)
|
||||
collapsed = deck.get("browserCollapsed", False)
|
||||
|
@ -504,57 +498,6 @@ class DeckManager:
|
|||
def for_card_ids(self, cids: List[int]) -> List[int]:
|
||||
return self.col.db.list(f"select did from cards where id in {ids2str(cids)}")
|
||||
|
||||
# fixme
|
||||
def _recoverOrphans(self) -> None:
|
||||
pass
|
||||
# dids = list(self.decks.keys())
|
||||
# mod = self.col.db.mod
|
||||
# self.col.db.execute(
|
||||
# "update cards set did = 1 where did not in " + ids2str(dids)
|
||||
# )
|
||||
# self.col.db.mod = mod
|
||||
|
||||
def checkIntegrity(self) -> None:
|
||||
self._recoverOrphans()
|
||||
|
||||
def should_deck_be_displayed(
|
||||
self, deck, force_default: bool = True, assume_no_child: bool = False
|
||||
) -> bool:
|
||||
"""Whether the deck should appear in main window, browser side list, filter, deck selection...
|
||||
|
||||
True, except for empty default deck without children"""
|
||||
if deck["id"] != "1":
|
||||
return True
|
||||
return self.should_default_be_displayed(force_default, assume_no_child)
|
||||
|
||||
def should_default_be_displayed(
|
||||
self,
|
||||
force_default: bool = True,
|
||||
assume_no_child: bool = False,
|
||||
default_deck: Optional[Dict[str, Any]] = None,
|
||||
) -> bool:
|
||||
"""Whether the default deck should appear in main window, browser side list, filter, deck selection...
|
||||
|
||||
True, except for empty default deck (without children)"""
|
||||
if force_default:
|
||||
return True
|
||||
if self.col.db.scalar("select 1 from cards where did = 1 limit 1"):
|
||||
return True
|
||||
# fixme
|
||||
return False
|
||||
# if len(self.all_names_and_ids()) == 1:
|
||||
# return True
|
||||
# # looking for children
|
||||
# if assume_no_child:
|
||||
# return False
|
||||
# if default_deck is None:
|
||||
# default_deck = self.get(1)
|
||||
# defaultName = default_deck["name"]
|
||||
# for name in self.allNames():
|
||||
# if name.startswith(f"{defaultName}::"):
|
||||
# return True
|
||||
# return False
|
||||
|
||||
# Deck selection
|
||||
#############################################################
|
||||
|
||||
|
@ -673,11 +616,3 @@ class DeckManager:
|
|||
|
||||
def isDyn(self, did: Union[int, str]) -> Any:
|
||||
return self.get(did)["dyn"]
|
||||
|
||||
@staticmethod
|
||||
def normalizeName(name: str) -> str:
|
||||
return unicodedata.normalize("NFC", name.lower())
|
||||
|
||||
@staticmethod
|
||||
def equalName(name1: str, name2: str) -> bool:
|
||||
return DeckManager.normalizeName(name1) == DeckManager.normalizeName(name2)
|
||||
|
|
|
@ -686,12 +686,17 @@ class RustBackend:
|
|||
except NotFoundError:
|
||||
return None
|
||||
|
||||
def get_deck_names_and_ids(self) -> List[pb.DeckNameID]:
|
||||
return list(
|
||||
self._run_command(
|
||||
pb.BackendInput(get_deck_names=pb.Empty())
|
||||
).get_deck_names.entries
|
||||
)
|
||||
def get_deck_names_and_ids(
|
||||
self, skip_empty_default: bool, include_filtered: bool = True
|
||||
) -> Sequence[pb.DeckNameID]:
|
||||
return self._run_command(
|
||||
pb.BackendInput(
|
||||
get_deck_names=pb.GetDeckNamesIn(
|
||||
skip_empty_default=skip_empty_default,
|
||||
include_filtered=include_filtered,
|
||||
)
|
||||
)
|
||||
).get_deck_names.entries
|
||||
|
||||
def add_or_update_deck_legacy(
|
||||
self, deck: Dict[str, Any], preserve_usn: bool
|
||||
|
|
|
@ -395,7 +395,9 @@ limit %d"""
|
|||
else:
|
||||
# benchmarks indicate it's about 10x faster to search all decks
|
||||
# with the index than scan the table
|
||||
extra = " and did in " + ids2str(self.col.decks.allIds())
|
||||
extra = " and did in " + ids2str(
|
||||
d.id for d in self.col.decks.all_names_and_ids()
|
||||
)
|
||||
# review cards in relearning
|
||||
self.col.db.execute(
|
||||
f"""
|
||||
|
|
|
@ -65,9 +65,10 @@ def test_rename():
|
|||
# should be able to rename into a completely different branch, creating
|
||||
# parents as necessary
|
||||
d.decks.rename(d.decks.get(id), "foo::bar")
|
||||
assert "foo" in d.decks.allNames()
|
||||
assert "foo::bar" in d.decks.allNames()
|
||||
assert "hello::world" not in d.decks.allNames()
|
||||
names = [n.name for n in d.decks.all_names_and_ids()]
|
||||
assert "foo" in names
|
||||
assert "foo::bar" in names
|
||||
assert "hello::world" not in names
|
||||
# create another deck
|
||||
id = d.decks.id("tmp")
|
||||
# we can't rename it if it conflicts
|
||||
|
@ -76,8 +77,9 @@ def test_rename():
|
|||
d.decks.id("one::two::three")
|
||||
id = d.decks.id("one")
|
||||
d.decks.rename(d.decks.get(id), "yo")
|
||||
names = [n.name for n in d.decks.all_names_and_ids()]
|
||||
for n in "yo", "yo::two", "yo::two::three":
|
||||
assert n in d.decks.allNames()
|
||||
assert n in names
|
||||
# over filtered
|
||||
filteredId = d.decks.newDyn("filtered")
|
||||
filtered = d.decks.get(filteredId)
|
||||
|
@ -96,7 +98,7 @@ def test_renameForDragAndDrop():
|
|||
d = getEmptyCol()
|
||||
|
||||
def deckNames():
|
||||
return [name for name in sorted(d.decks.allNames()) if name != "Default"]
|
||||
return [n.name for n in d.decks.all_names_and_ids(skip_empty_default=True)]
|
||||
|
||||
languages_did = d.decks.id("Languages")
|
||||
chinese_did = d.decks.id("Chinese")
|
||||
|
@ -151,16 +153,3 @@ def test_renameForDragAndDrop():
|
|||
# '' is a convenient alias for the top level DID
|
||||
d.decks.renameForDragAndDrop(hsk_did, "")
|
||||
assert deckNames() == ["Chinese", "HSK", "Languages"]
|
||||
|
||||
|
||||
def test_check():
|
||||
d = getEmptyCol()
|
||||
|
||||
# currently disabled - see 5418af00f733ca62b0c087d1422feae01d6571b0
|
||||
# foo_did = d.decks.id("foo")
|
||||
# FOO_did = d.decks.id("bar")
|
||||
# FOO = d.decks.byName("bar")
|
||||
# FOO["name"] = "FOO"
|
||||
# d.decks.save(FOO)
|
||||
# d.decks._checkDeckTree()
|
||||
# assert "foo" not in d.decks.allNames() or "FOO" not in d.decks.allNames()
|
||||
|
|
|
@ -441,9 +441,9 @@ def test_review_limits():
|
|||
c.flush()
|
||||
|
||||
tree = d.sched.deckDueTree()
|
||||
# (('Default', 1, 0, 0, 0, ()), ('parent', 1514457677462, 5, 0, 0, (('child', 1514457677463, 5, 0, 0, ()),)))
|
||||
assert tree[1][2] == 5 # parent
|
||||
assert tree[1][5][0][2] == 5 # child
|
||||
# (('parent', 1514457677462, 5, 0, 0, (('child', 1514457677463, 5, 0, 0, ()),)))
|
||||
assert tree[0][2] == 5 # parent
|
||||
assert tree[0][5][0][2] == 5 # child
|
||||
|
||||
# .counts() should match
|
||||
d.decks.select(child["id"])
|
||||
|
@ -456,8 +456,8 @@ def test_review_limits():
|
|||
assert d.sched.counts() == (0, 0, 4)
|
||||
|
||||
tree = d.sched.deckDueTree()
|
||||
assert tree[1][2] == 4 # parent
|
||||
assert tree[1][5][0][2] == 4 # child
|
||||
assert tree[0][2] == 4 # parent
|
||||
assert tree[0][5][0][2] == 4 # child
|
||||
|
||||
|
||||
def test_button_spacing():
|
||||
|
|
|
@ -1148,11 +1148,6 @@ QTableView {{ gridline-color: {grid} }}
|
|||
|
||||
def fillGroups(root, nodes: Sequence[DeckTreeNode], head=""):
|
||||
for node in nodes:
|
||||
if node.deck_id == 1 and not node.children:
|
||||
if not self.mw.col.decks.should_default_be_displayed(
|
||||
force_default=False, assume_no_child=True
|
||||
):
|
||||
continue
|
||||
|
||||
def set_filter():
|
||||
full_name = head + node.name # pylint: disable=cell-var-from-loop
|
||||
|
|
|
@ -49,7 +49,8 @@ class ExportDialog(QDialog):
|
|||
self.exporterChanged(idx)
|
||||
# deck list
|
||||
if self.cids is None:
|
||||
self.decks = [_("All Decks")] + sorted(self.col.decks.allNames())
|
||||
self.decks = [_("All Decks")]
|
||||
self.decks.extend(d.name for d in self.col.decks.all_names_and_ids())
|
||||
else:
|
||||
self.decks = [_("Selected Notes")]
|
||||
self.frm.deck.addItems(self.decks)
|
||||
|
|
|
@ -1115,8 +1115,7 @@ title="%s" %s>%s</button>""" % (
|
|||
if not search:
|
||||
if not deck["dyn"]:
|
||||
search = 'deck:"%s" ' % deck["name"]
|
||||
decks = self.col.decks.allNames()
|
||||
while _("Filtered Deck %d") % n in decks:
|
||||
while self.col.decks.id_for_name(_("Filtered Deck %d") % n):
|
||||
n += 1
|
||||
name = _("Filtered Deck %d") % n
|
||||
did = self.col.decks.newDyn(name)
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import aqt
|
||||
from anki.decks import DeckManager
|
||||
from anki.lang import _
|
||||
from aqt import gui_hooks
|
||||
from aqt.qt import *
|
||||
|
@ -52,10 +51,12 @@ class StudyDeck(QDialog):
|
|||
if title:
|
||||
self.setWindowTitle(title)
|
||||
if not names:
|
||||
names = sorted(
|
||||
self.mw.col.decks.allNames(dyn=dyn, force_default=False),
|
||||
key=DeckManager._path,
|
||||
)
|
||||
names = [
|
||||
d.name
|
||||
for d in self.mw.col.decks.all_names_and_ids(
|
||||
include_filtered=dyn, skip_empty_default=True
|
||||
)
|
||||
]
|
||||
self.nameFunc = None
|
||||
self.origNames = names
|
||||
else:
|
||||
|
|
|
@ -32,9 +32,9 @@ class TagEdit(QLineEdit):
|
|||
"Set the current col, updating list of available tags."
|
||||
self.col = col
|
||||
if self.type == 0:
|
||||
l = sorted(self.col.tags.all())
|
||||
l = self.col.tags.all()
|
||||
else:
|
||||
l = sorted(self.col.decks.allNames())
|
||||
l = (d.name for d in self.col.decks.all_names_and_ids())
|
||||
self.model.setStringList(l)
|
||||
|
||||
def focusInEvent(self, evt):
|
||||
|
|
|
@ -341,7 +341,7 @@ impl Backend {
|
|||
Value::GetDeckIdByName(name) => {
|
||||
OValue::GetDeckIdByName(self.get_deck_id_by_name(&name)?)
|
||||
}
|
||||
Value::GetDeckNames(_) => OValue::GetDeckNames(self.get_deck_names()?),
|
||||
Value::GetDeckNames(input) => OValue::GetDeckNames(self.get_deck_names(input)?),
|
||||
Value::AddOrUpdateDeckLegacy(input) => {
|
||||
OValue::AddOrUpdateDeckLegacy(self.add_or_update_deck_legacy(input)?)
|
||||
}
|
||||
|
@ -1011,9 +1011,13 @@ impl Backend {
|
|||
})
|
||||
}
|
||||
|
||||
fn get_deck_names(&self) -> Result<pb::DeckNames> {
|
||||
fn get_deck_names(&self, input: pb::GetDeckNamesIn) -> Result<pb::DeckNames> {
|
||||
self.with_col(|col| {
|
||||
let names = col.storage.get_all_deck_names()?;
|
||||
let names = if input.include_filtered {
|
||||
col.get_all_deck_names(input.skip_empty_default)?
|
||||
} else {
|
||||
col.get_all_normal_deck_names()?
|
||||
};
|
||||
Ok(pb::DeckNames {
|
||||
entries: names
|
||||
.into_iter()
|
||||
|
|
|
@ -187,6 +187,10 @@ fn immediate_parent_name(machine_name: &str) -> Option<&str> {
|
|||
}
|
||||
|
||||
impl Collection {
|
||||
pub(crate) fn default_deck_is_empty(&self) -> Result<bool> {
|
||||
self.storage.deck_is_empty(DeckID(1))
|
||||
}
|
||||
|
||||
pub(crate) fn add_or_update_deck(&mut self, deck: &mut Deck, preserve_usn: bool) -> Result<()> {
|
||||
// fixme: vet cache clearing
|
||||
self.state.deck_cache.clear();
|
||||
|
@ -416,6 +420,32 @@ impl Collection {
|
|||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_all_deck_names(&self, skip_empty_default: bool) -> Result<Vec<(DeckID, String)>> {
|
||||
if skip_empty_default && self.default_deck_is_empty()? {
|
||||
Ok(self
|
||||
.storage
|
||||
.get_all_deck_names()?
|
||||
.into_iter()
|
||||
.filter(|(id, _name)| id.0 != 1)
|
||||
.collect())
|
||||
} else {
|
||||
self.storage.get_all_deck_names()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_all_normal_deck_names(&mut self) -> Result<Vec<(DeckID, String)>> {
|
||||
Ok(self
|
||||
.storage
|
||||
.get_all_deck_names()?
|
||||
.into_iter()
|
||||
.filter(|(id, _name)| id.0 != 1)
|
||||
.filter(|(id, _name)| match self.get_deck(*id) {
|
||||
Ok(Some(deck)) => !deck.is_filtered(),
|
||||
_ => true,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -151,6 +151,21 @@ fn remaining_counts_for_deck(
|
|||
}
|
||||
}
|
||||
|
||||
fn hide_default_deck(node: &mut DeckTreeNode) {
|
||||
for (idx, child) in node.children.iter().enumerate() {
|
||||
// we can hide the default if it has no children
|
||||
if child.deck_id == 1 && child.children.is_empty() {
|
||||
if child.level == 1 && node.children.len() == 1 {
|
||||
// can't remove if there are no other decks
|
||||
} else {
|
||||
// safe to remove
|
||||
node.children.remove(idx);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize_tuple)]
|
||||
pub(crate) struct LegacyDueCounts {
|
||||
name: String,
|
||||
|
@ -187,6 +202,9 @@ impl Collection {
|
|||
.collect();
|
||||
|
||||
add_collapsed(&mut tree, &decks_map, !counts);
|
||||
if self.default_deck_is_empty()? {
|
||||
hide_default_deck(&mut tree);
|
||||
}
|
||||
|
||||
if counts {
|
||||
let counts = self.due_counts()?;
|
||||
|
@ -251,8 +269,7 @@ mod test {
|
|||
|
||||
let tree = col.deck_tree(false)?;
|
||||
|
||||
// 4 including default
|
||||
assert_eq!(tree.children.len(), 4);
|
||||
assert_eq!(tree.children.len(), 3);
|
||||
|
||||
assert_eq!(tree.children[1].name, "2");
|
||||
assert_eq!(tree.children[1].children[0].name, "a");
|
||||
|
@ -274,7 +291,7 @@ mod test {
|
|||
col.storage.remove_deck(col.get_deck_id("2::3")?.unwrap())?;
|
||||
|
||||
let tree = col.deck_tree(false)?;
|
||||
assert_eq!(tree.children.len(), 2);
|
||||
assert_eq!(tree.children.len(), 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -178,6 +178,15 @@ impl SqliteStorage {
|
|||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn deck_is_empty(&self, did: DeckID) -> Result<bool> {
|
||||
self.db
|
||||
.prepare_cached("select null from cards where did=?")?
|
||||
.query(&[did])?
|
||||
.next()
|
||||
.map(|o| o.is_none())
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
// Upgrading/downgrading/legacy
|
||||
|
||||
pub(super) fn add_default_deck(&self, i18n: &I18n) -> Result<()> {
|
||||
|
|
Loading…
Reference in a new issue