mirror of
https://github.com/ankitects/anki.git
synced 2025-09-21 15:32:23 -04:00
move filtered deck empty/fill to backend
emptying of individual card ids still to be done
This commit is contained in:
parent
8d2867aa2d
commit
8f9037cf0f
10 changed files with 269 additions and 231 deletions
|
@ -103,6 +103,8 @@ service BackendService {
|
||||||
rpc RestoreBuriedAndSuspendedCards (CardIDs) returns (Empty);
|
rpc RestoreBuriedAndSuspendedCards (CardIDs) returns (Empty);
|
||||||
rpc UnburyCardsInCurrentDeck (UnburyCardsInCurrentDeckIn) returns (Empty);
|
rpc UnburyCardsInCurrentDeck (UnburyCardsInCurrentDeckIn) returns (Empty);
|
||||||
rpc BuryOrSuspendCards (BuryOrSuspendCardsIn) returns (Empty);
|
rpc BuryOrSuspendCards (BuryOrSuspendCardsIn) returns (Empty);
|
||||||
|
rpc EmptyFilteredDeck (DeckID) returns (Empty);
|
||||||
|
rpc RebuildFilteredDeck (DeckID) returns (UInt32);
|
||||||
|
|
||||||
// stats
|
// stats
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ from __future__ import annotations
|
||||||
import random
|
import random
|
||||||
import time
|
import time
|
||||||
from heapq import *
|
from heapq import *
|
||||||
from typing import Any, List, Optional, Sequence, Tuple, Union
|
from typing import Any, List, Optional, Tuple, Union
|
||||||
|
|
||||||
import anki
|
import anki
|
||||||
from anki import hooks
|
from anki import hooks
|
||||||
|
@ -599,77 +599,9 @@ did = ? and queue = {QUEUE_TYPE_REV} and due <= ? limit ?""",
|
||||||
idealIvl = self._fuzzedIvl(idealIvl)
|
idealIvl = self._fuzzedIvl(idealIvl)
|
||||||
return idealIvl
|
return idealIvl
|
||||||
|
|
||||||
# Dynamic deck handling
|
# Filtered deck handling
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def rebuildDyn(self, did: Optional[int] = None) -> Optional[Sequence[int]]: # type: ignore[override]
|
|
||||||
"Rebuild a dynamic deck."
|
|
||||||
did = did or self.col.decks.selected()
|
|
||||||
deck = self.col.decks.get(did)
|
|
||||||
assert deck["dyn"]
|
|
||||||
# move any existing cards back first, then fill
|
|
||||||
self.emptyDyn(did)
|
|
||||||
ids = self._fillDyn(deck)
|
|
||||||
if not ids:
|
|
||||||
return None
|
|
||||||
# and change to our new deck
|
|
||||||
self.col.decks.select(did)
|
|
||||||
return ids
|
|
||||||
|
|
||||||
def _fillDyn(self, deck: Deck) -> Sequence[int]: # type: ignore[override]
|
|
||||||
search, limit, order = deck["terms"][0]
|
|
||||||
orderlimit = self._dynOrder(order, limit)
|
|
||||||
if search.strip():
|
|
||||||
search = "(%s)" % search
|
|
||||||
search = "%s -is:suspended -is:buried -deck:filtered -is:learn" % search
|
|
||||||
try:
|
|
||||||
ids = self.col.findCards(search, order=orderlimit)
|
|
||||||
except:
|
|
||||||
ids = []
|
|
||||||
return ids
|
|
||||||
# move the cards over
|
|
||||||
self.col.log(deck["id"], ids)
|
|
||||||
self._moveToDyn(deck["id"], ids)
|
|
||||||
return ids
|
|
||||||
|
|
||||||
def emptyDyn(self, did: Optional[int], lim: Optional[str] = None) -> None:
|
|
||||||
if not lim:
|
|
||||||
lim = "did = %s" % did
|
|
||||||
self.col.log(self.col.db.list("select id from cards where %s" % lim))
|
|
||||||
# move out of cram queue
|
|
||||||
self.col.db.execute(
|
|
||||||
f"""
|
|
||||||
update cards set did = odid, queue = (case when type = {CARD_TYPE_LRN} then {QUEUE_TYPE_NEW}
|
|
||||||
else type end), type = (case when type = {CARD_TYPE_LRN} then {CARD_TYPE_NEW} else type end),
|
|
||||||
due = odue, odue = 0, odid = 0, usn = ? where %s"""
|
|
||||||
% lim,
|
|
||||||
self.col.usn(),
|
|
||||||
)
|
|
||||||
|
|
||||||
def _moveToDyn(self, did: int, ids: Sequence[int]) -> None: # type: ignore[override]
|
|
||||||
deck = self.col.decks.get(did)
|
|
||||||
data = []
|
|
||||||
t = intTime()
|
|
||||||
u = self.col.usn()
|
|
||||||
for c, id in enumerate(ids):
|
|
||||||
# start at -100000 so that reviews are all due
|
|
||||||
data.append((did, -100000 + c, u, id))
|
|
||||||
# due reviews stay in the review queue. careful: can't use
|
|
||||||
# "odid or did", as sqlite converts to boolean
|
|
||||||
queue = f"""
|
|
||||||
(case when type={CARD_TYPE_REV} and (case when odue then odue <= %d else due <= %d end)
|
|
||||||
then {QUEUE_TYPE_REV} else {QUEUE_TYPE_NEW} end)"""
|
|
||||||
queue %= (self.today, self.today)
|
|
||||||
self.col.db.executemany(
|
|
||||||
"""
|
|
||||||
update cards set
|
|
||||||
odid = (case when odid then odid else did end),
|
|
||||||
odue = (case when odue then odue else due end),
|
|
||||||
did = ?, queue = %s, due = ?, usn = ? where id = ?"""
|
|
||||||
% queue,
|
|
||||||
data,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _dynIvlBoost(self, card: Card) -> int:
|
def _dynIvlBoost(self, card: Card) -> int:
|
||||||
assert card.odid and card.type == CARD_TYPE_REV
|
assert card.odid and card.type == CARD_TYPE_REV
|
||||||
assert card.factor
|
assert card.factor
|
||||||
|
|
|
@ -25,7 +25,7 @@ import anki.backend_pb2 as pb
|
||||||
from anki import hooks
|
from anki import hooks
|
||||||
from anki.cards import Card
|
from anki.cards import Card
|
||||||
from anki.consts import *
|
from anki.consts import *
|
||||||
from anki.decks import Deck, DeckConfig, DeckManager, FilteredDeck, QueueConfig
|
from anki.decks import Deck, DeckConfig, DeckManager, QueueConfig
|
||||||
from anki.lang import _
|
from anki.lang import _
|
||||||
from anki.notes import Note
|
from anki.notes import Note
|
||||||
from anki.rsbackend import (
|
from anki.rsbackend import (
|
||||||
|
@ -1062,7 +1062,7 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l
|
||||||
|
|
||||||
return ivl
|
return ivl
|
||||||
|
|
||||||
# Dynamic deck handling
|
# Filtered deck handling
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
_restoreQueueWhenEmptyingSnippet = f"""
|
_restoreQueueWhenEmptyingSnippet = f"""
|
||||||
|
@ -1076,41 +1076,19 @@ end)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def rebuildDyn(self, did: Optional[int] = None) -> Optional[int]:
|
def rebuildDyn(self, did: Optional[int] = None) -> Optional[int]:
|
||||||
"Rebuild a dynamic deck."
|
"Rebuild a filtered deck."
|
||||||
did = did or self.col.decks.selected()
|
did = did or self.col.decks.selected()
|
||||||
deck = self.col.decks.get(did)
|
count = self.col.backend.rebuild_filtered_deck(did) or None
|
||||||
assert deck["dyn"]
|
if not count:
|
||||||
# move any existing cards back first, then fill
|
|
||||||
self.emptyDyn(did)
|
|
||||||
cnt = self._fillDyn(deck)
|
|
||||||
if not cnt:
|
|
||||||
return None
|
return None
|
||||||
# and change to our new deck
|
# and change to our new deck
|
||||||
self.col.decks.select(did)
|
self.col.decks.select(did)
|
||||||
return cnt
|
return count
|
||||||
|
|
||||||
def _fillDyn(self, deck: FilteredDeck) -> int:
|
|
||||||
start = -100000
|
|
||||||
total = 0
|
|
||||||
for search, limit, order in deck["terms"]:
|
|
||||||
orderlimit = self._dynOrder(order, limit)
|
|
||||||
if search.strip():
|
|
||||||
search = "(%s)" % search
|
|
||||||
search = "%s -is:suspended -is:buried -deck:filtered" % search
|
|
||||||
try:
|
|
||||||
ids = self.col.findCards(search, order=orderlimit)
|
|
||||||
except:
|
|
||||||
return total
|
|
||||||
# move the cards over
|
|
||||||
self.col.log(deck["id"], ids)
|
|
||||||
self._moveToDyn(deck["id"], ids, start=start + total)
|
|
||||||
total += len(ids)
|
|
||||||
return total
|
|
||||||
|
|
||||||
def emptyDyn(self, did: Optional[int], lim: Optional[str] = None) -> None:
|
def emptyDyn(self, did: Optional[int], lim: Optional[str] = None) -> None:
|
||||||
if not lim:
|
if lim is None:
|
||||||
lim = "did = %s" % did
|
self.col.backend.empty_filtered_deck(did)
|
||||||
self.col.log(self.col.db.list("select id from cards where %s" % lim))
|
return
|
||||||
|
|
||||||
self.col.db.execute(
|
self.col.db.execute(
|
||||||
"""
|
"""
|
||||||
|
@ -1123,57 +1101,6 @@ due = (case when odue>0 then odue else due end), odue = 0, odid = 0, usn = ? whe
|
||||||
def remFromDyn(self, cids: List[int]) -> None:
|
def remFromDyn(self, cids: List[int]) -> None:
|
||||||
self.emptyDyn(None, "id in %s and odid" % ids2str(cids))
|
self.emptyDyn(None, "id in %s and odid" % ids2str(cids))
|
||||||
|
|
||||||
def _dynOrder(self, o: int, l: int) -> str:
|
|
||||||
if o == DYN_OLDEST:
|
|
||||||
t = "(select max(id) from revlog where cid=c.id)"
|
|
||||||
elif o == DYN_RANDOM:
|
|
||||||
t = "random()"
|
|
||||||
elif o == DYN_SMALLINT:
|
|
||||||
t = "ivl"
|
|
||||||
elif o == DYN_BIGINT:
|
|
||||||
t = "ivl desc"
|
|
||||||
elif o == DYN_LAPSES:
|
|
||||||
t = "lapses desc"
|
|
||||||
elif o == DYN_ADDED:
|
|
||||||
t = "n.id"
|
|
||||||
elif o == DYN_REVADDED:
|
|
||||||
t = "n.id desc"
|
|
||||||
elif o == DYN_DUEPRIORITY:
|
|
||||||
t = (
|
|
||||||
f"(case when queue={QUEUE_TYPE_REV} and due <= %d then (ivl / cast(%d-due+0.001 as real)) else 100000+due end)"
|
|
||||||
% (self.today, self.today)
|
|
||||||
)
|
|
||||||
else: # DYN_DUE or unknown
|
|
||||||
t = "c.due, c.ord"
|
|
||||||
return t + " limit %d" % l
|
|
||||||
|
|
||||||
def _moveToDyn(self, did: int, ids: Sequence[int], start: int = -100000) -> None:
|
|
||||||
deck = self.col.decks.get(did)
|
|
||||||
data = []
|
|
||||||
u = self.col.usn()
|
|
||||||
due = start
|
|
||||||
for id in ids:
|
|
||||||
data.append((did, due, u, id))
|
|
||||||
due += 1
|
|
||||||
|
|
||||||
queue = ""
|
|
||||||
if not deck["resched"]:
|
|
||||||
queue = f",queue={QUEUE_TYPE_REV}"
|
|
||||||
|
|
||||||
query = (
|
|
||||||
"""
|
|
||||||
update cards set
|
|
||||||
odid = did, odue = due,
|
|
||||||
did = ?,
|
|
||||||
due = (case when due <= 0 then due else ? end),
|
|
||||||
usn = ?
|
|
||||||
%s
|
|
||||||
where id = ?
|
|
||||||
"""
|
|
||||||
% queue
|
|
||||||
)
|
|
||||||
self.col.db.executemany(query, data)
|
|
||||||
|
|
||||||
def _removeFromFiltered(self, card: Card) -> None:
|
def _removeFromFiltered(self, card: Card) -> None:
|
||||||
if card.odid:
|
if card.odid:
|
||||||
card.did = card.odid
|
card.did = card.odid
|
||||||
|
|
|
@ -539,6 +539,14 @@ impl BackendService for Backend {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn empty_filtered_deck(&mut self, input: pb::DeckId) -> BackendResult<Empty> {
|
||||||
|
self.with_col(|col| col.empty_filtered_deck(input.did.into()).map(Into::into))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rebuild_filtered_deck(&mut self, input: pb::DeckId) -> BackendResult<pb::UInt32> {
|
||||||
|
self.with_col(|col| col.rebuild_filtered_deck(input.did.into()).map(Into::into))
|
||||||
|
}
|
||||||
|
|
||||||
// statistics
|
// statistics
|
||||||
//-----------------------------------------------
|
//-----------------------------------------------
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// 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
|
||||||
|
|
||||||
use crate::decks::DeckID;
|
use crate::decks::{DeckFilterContext, DeckID};
|
||||||
use crate::define_newtype;
|
use crate::define_newtype;
|
||||||
use crate::err::{AnkiError, Result};
|
use crate::err::{AnkiError, Result};
|
||||||
use crate::notes::NoteID;
|
use crate::notes::NoteID;
|
||||||
|
@ -102,7 +102,42 @@ impl Card {
|
||||||
self.usn = usn;
|
self.usn = usn;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn return_home(&mut self, sched: SchedulerVersion) {
|
pub(crate) fn move_into_filtered_deck(&mut self, ctx: &DeckFilterContext, position: i32) {
|
||||||
|
// filtered and v1 learning cards are excluded, so odue should be guaranteed to be zero
|
||||||
|
if self.original_due != 0 {
|
||||||
|
println!("bug: odue was set");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.original_deck_id = self.deck_id;
|
||||||
|
self.deck_id = ctx.target_deck;
|
||||||
|
|
||||||
|
self.original_due = self.due;
|
||||||
|
|
||||||
|
if ctx.scheduler == SchedulerVersion::V1 {
|
||||||
|
if self.ctype == CardType::Review && self.due <= ctx.today as i32 {
|
||||||
|
// review cards that are due are left in the review queue
|
||||||
|
} else {
|
||||||
|
// new + non-due go into new queue
|
||||||
|
self.queue = CardQueue::New;
|
||||||
|
}
|
||||||
|
if self.due != 0 {
|
||||||
|
self.due = position;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// if rescheduling is disabled, all cards go in the review queue
|
||||||
|
if !ctx.config.reschedule {
|
||||||
|
self.queue = CardQueue::Review;
|
||||||
|
}
|
||||||
|
// fixme: can we unify this with v1 scheduler in the future?
|
||||||
|
// https://anki.tenderapp.com/discussions/ankidesktop/35978-rebuilding-filtered-deck-on-experimental-v2-empties-deck-and-reschedules-to-the-year-1745
|
||||||
|
if self.due > 0 {
|
||||||
|
self.due = position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn remove_from_filtered_deck(&mut self, sched: SchedulerVersion) {
|
||||||
if self.original_deck_id.0 == 0 {
|
if self.original_deck_id.0 == 0 {
|
||||||
// not in a filtered deck
|
// not in a filtered deck
|
||||||
return;
|
return;
|
||||||
|
@ -110,14 +145,11 @@ impl Card {
|
||||||
|
|
||||||
self.deck_id = self.original_deck_id;
|
self.deck_id = self.original_deck_id;
|
||||||
self.original_deck_id.0 = 0;
|
self.original_deck_id.0 = 0;
|
||||||
if self.original_due > 0 {
|
|
||||||
self.due = self.original_due;
|
|
||||||
}
|
|
||||||
self.original_due = 0;
|
|
||||||
|
|
||||||
self.queue = match sched {
|
match sched {
|
||||||
SchedulerVersion::V1 => {
|
SchedulerVersion::V1 => {
|
||||||
match self.ctype {
|
self.due = self.original_due;
|
||||||
|
self.queue = match self.ctype {
|
||||||
CardType::New => CardQueue::New,
|
CardType::New => CardQueue::New,
|
||||||
CardType::Learn => CardQueue::New,
|
CardType::Learn => CardQueue::New,
|
||||||
CardType::Review => CardQueue::Review,
|
CardType::Review => CardQueue::Review,
|
||||||
|
@ -126,11 +158,19 @@ impl Card {
|
||||||
println!("did not expect relearn type in v1 for card {}", self.id);
|
println!("did not expect relearn type in v1 for card {}", self.id);
|
||||||
CardQueue::New
|
CardQueue::New
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
if self.ctype == CardType::Learn {
|
||||||
|
self.ctype = CardType::New;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SchedulerVersion::V2 => {
|
SchedulerVersion::V2 => {
|
||||||
|
// original_due is cleared if card answered in filtered deck
|
||||||
|
if self.original_due > 0 {
|
||||||
|
self.due = self.original_due;
|
||||||
|
}
|
||||||
|
|
||||||
if (self.queue as i8) >= 0 {
|
if (self.queue as i8) >= 0 {
|
||||||
match self.ctype {
|
self.queue = match self.ctype {
|
||||||
CardType::Learn | CardType::Relearn => {
|
CardType::Learn | CardType::Relearn => {
|
||||||
if self.due > 1_000_000_000 {
|
if self.due > 1_000_000_000 {
|
||||||
// unix timestamp
|
// unix timestamp
|
||||||
|
@ -143,15 +183,11 @@ impl Card {
|
||||||
CardType::New => CardQueue::New,
|
CardType::New => CardQueue::New,
|
||||||
CardType::Review => CardQueue::Review,
|
CardType::Review => CardQueue::Review,
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
self.queue
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
if sched == SchedulerVersion::V1 && self.ctype == CardType::Learn {
|
self.original_due = 0;
|
||||||
self.ctype = CardType::New;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove the card from the (re)learning queue.
|
/// Remove the card from the (re)learning queue.
|
||||||
|
|
167
rslib/src/decks/filtered.rs
Normal file
167
rslib/src/decks/filtered.rs
Normal file
|
@ -0,0 +1,167 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
use super::{Deck, DeckID};
|
||||||
|
pub use crate::backend_proto::{
|
||||||
|
deck_kind::Kind as DeckKind, filtered_search_term::FilteredSearchOrder, Deck as DeckProto,
|
||||||
|
DeckCommon, DeckKind as DeckKindProto, FilteredDeck, FilteredSearchTerm, NormalDeck,
|
||||||
|
};
|
||||||
|
use crate::{
|
||||||
|
card::{CardID, CardQueue},
|
||||||
|
collection::Collection,
|
||||||
|
config::SchedulerVersion,
|
||||||
|
err::Result,
|
||||||
|
prelude::AnkiError,
|
||||||
|
search::SortMode,
|
||||||
|
timestamp::TimestampSecs,
|
||||||
|
types::Usn,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl Deck {
|
||||||
|
pub fn new_filtered() -> Deck {
|
||||||
|
let mut filt = FilteredDeck::default();
|
||||||
|
filt.search_terms.push(FilteredSearchTerm {
|
||||||
|
search: "".into(),
|
||||||
|
limit: 100,
|
||||||
|
order: 0,
|
||||||
|
});
|
||||||
|
filt.preview_delay = 10;
|
||||||
|
filt.reschedule = true;
|
||||||
|
Deck {
|
||||||
|
id: DeckID(0),
|
||||||
|
name: "".into(),
|
||||||
|
mtime_secs: TimestampSecs(0),
|
||||||
|
usn: Usn(0),
|
||||||
|
common: DeckCommon::default(),
|
||||||
|
kind: DeckKind::Filtered(filt),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_filtered(&self) -> bool {
|
||||||
|
matches!(self.kind, DeckKind::Filtered(_))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct DeckFilterContext<'a> {
|
||||||
|
pub target_deck: DeckID,
|
||||||
|
pub config: &'a FilteredDeck,
|
||||||
|
pub scheduler: SchedulerVersion,
|
||||||
|
pub usn: Usn,
|
||||||
|
pub today: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Collection {
|
||||||
|
pub fn empty_filtered_deck(&mut self, did: DeckID) -> Result<()> {
|
||||||
|
self.transact(None, |col| col.return_all_cards_in_filtered_deck(did))
|
||||||
|
}
|
||||||
|
pub(super) fn return_all_cards_in_filtered_deck(&mut self, did: DeckID) -> Result<()> {
|
||||||
|
let cids = self.storage.all_cards_in_single_deck(did)?;
|
||||||
|
self.return_cards_to_home_deck(&cids)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unlike the old Python code, this also marks the cards as modified.
|
||||||
|
fn return_cards_to_home_deck(&mut self, cids: &[CardID]) -> Result<()> {
|
||||||
|
let sched = self.sched_ver();
|
||||||
|
let usn = self.usn()?;
|
||||||
|
for cid in cids {
|
||||||
|
if let Some(mut card) = self.storage.get_card(*cid)? {
|
||||||
|
let original = card.clone();
|
||||||
|
card.remove_from_filtered_deck(sched);
|
||||||
|
self.update_card(&mut card, &original, usn)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unlike the old Python code, this also marks the cards as modified.
|
||||||
|
pub fn rebuild_filtered_deck(&mut self, did: DeckID) -> Result<u32> {
|
||||||
|
let deck = self.get_deck(did)?.ok_or(AnkiError::NotFound)?;
|
||||||
|
let config = if let DeckKind::Filtered(kind) = &deck.kind {
|
||||||
|
kind
|
||||||
|
} else {
|
||||||
|
return Err(AnkiError::invalid_input("not filtered"));
|
||||||
|
};
|
||||||
|
let ctx = DeckFilterContext {
|
||||||
|
target_deck: did,
|
||||||
|
config,
|
||||||
|
scheduler: self.sched_ver(),
|
||||||
|
usn: self.usn()?,
|
||||||
|
today: self.timing_today()?.days_elapsed,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.transact(None, |col| {
|
||||||
|
col.return_all_cards_in_filtered_deck(did)?;
|
||||||
|
col.build_filtered_deck(ctx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_filtered_deck(&mut self, ctx: DeckFilterContext) -> Result<u32> {
|
||||||
|
let start = -100_000;
|
||||||
|
let mut position = start;
|
||||||
|
for term in &ctx.config.search_terms {
|
||||||
|
position = self.move_cards_matching_term(&ctx, term, position)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((position - start) as u32)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move matching cards into filtered deck.
|
||||||
|
/// Returns the new starting position.
|
||||||
|
fn move_cards_matching_term(
|
||||||
|
&mut self,
|
||||||
|
ctx: &DeckFilterContext,
|
||||||
|
term: &FilteredSearchTerm,
|
||||||
|
mut position: i32,
|
||||||
|
) -> Result<i32> {
|
||||||
|
let search = format!(
|
||||||
|
"{} -is:suspended -is:buried -deck:filtered {}",
|
||||||
|
if term.search.trim().is_empty() {
|
||||||
|
"".to_string()
|
||||||
|
} else {
|
||||||
|
format!("({})", term.search)
|
||||||
|
},
|
||||||
|
if ctx.scheduler == SchedulerVersion::V1 {
|
||||||
|
"-is:learn"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let order = order_and_limit_for_search(term, ctx.today);
|
||||||
|
|
||||||
|
self.search_cards_into_table(&search, SortMode::Custom(order))?;
|
||||||
|
for mut card in self.storage.all_searched_cards()? {
|
||||||
|
let original = card.clone();
|
||||||
|
card.move_into_filtered_deck(ctx, position);
|
||||||
|
self.update_card(&mut card, &original, ctx.usn)?;
|
||||||
|
position += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn order_and_limit_for_search(term: &FilteredSearchTerm, today: u32) -> String {
|
||||||
|
let temp_string;
|
||||||
|
let order = match term.order() {
|
||||||
|
FilteredSearchOrder::OldestFirst => "(select max(id) from revlog where cid=c.id)",
|
||||||
|
FilteredSearchOrder::Random => "random()",
|
||||||
|
FilteredSearchOrder::IntervalsAscending => "ivl",
|
||||||
|
FilteredSearchOrder::IntervalsDescending => "ivl desc",
|
||||||
|
FilteredSearchOrder::Lapses => "lapses desc",
|
||||||
|
FilteredSearchOrder::Added => "n.id",
|
||||||
|
FilteredSearchOrder::ReverseAdded => "n.id desc",
|
||||||
|
FilteredSearchOrder::Due => "c.due, c.ord",
|
||||||
|
FilteredSearchOrder::DuePriority => {
|
||||||
|
temp_string = format!(
|
||||||
|
"
|
||||||
|
(case when queue={rev_queue} and due <= {today}
|
||||||
|
then (ivl / cast({today}-due+0.001 as real)) else 100000+due end)",
|
||||||
|
rev_queue = CardQueue::Review as i8,
|
||||||
|
today = today
|
||||||
|
);
|
||||||
|
&temp_string
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
format!("{} limit {}", order, term.limit)
|
||||||
|
}
|
|
@ -7,7 +7,6 @@ pub use crate::backend_proto::{
|
||||||
DeckCommon, DeckKind as DeckKindProto, FilteredDeck, FilteredSearchTerm, NormalDeck,
|
DeckCommon, DeckKind as DeckKindProto, FilteredDeck, FilteredSearchTerm, NormalDeck,
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
card::CardID,
|
|
||||||
collection::Collection,
|
collection::Collection,
|
||||||
deckconf::DeckConfID,
|
deckconf::DeckConfID,
|
||||||
define_newtype,
|
define_newtype,
|
||||||
|
@ -18,9 +17,11 @@ use crate::{
|
||||||
types::Usn,
|
types::Usn,
|
||||||
};
|
};
|
||||||
mod counts;
|
mod counts;
|
||||||
|
mod filtered;
|
||||||
mod schema11;
|
mod schema11;
|
||||||
mod tree;
|
mod tree;
|
||||||
pub(crate) use counts::DueCounts;
|
pub(crate) use counts::DueCounts;
|
||||||
|
pub(crate) use filtered::DeckFilterContext;
|
||||||
pub use schema11::DeckSchema11;
|
pub use schema11::DeckSchema11;
|
||||||
use std::{borrow::Cow, sync::Arc};
|
use std::{borrow::Cow, sync::Arc};
|
||||||
|
|
||||||
|
@ -51,25 +52,6 @@ impl Deck {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_filtered() -> Deck {
|
|
||||||
let mut filt = FilteredDeck::default();
|
|
||||||
filt.search_terms.push(FilteredSearchTerm {
|
|
||||||
search: "".into(),
|
|
||||||
limit: 100,
|
|
||||||
order: 0,
|
|
||||||
});
|
|
||||||
filt.preview_delay = 10;
|
|
||||||
filt.reschedule = true;
|
|
||||||
Deck {
|
|
||||||
id: DeckID(0),
|
|
||||||
name: "".into(),
|
|
||||||
mtime_secs: TimestampSecs(0),
|
|
||||||
usn: Usn(0),
|
|
||||||
common: DeckCommon::default(),
|
|
||||||
kind: DeckKind::Filtered(filt),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn reset_stats_if_day_changed(&mut self, today: u32) {
|
fn reset_stats_if_day_changed(&mut self, today: u32) {
|
||||||
let c = &mut self.common;
|
let c = &mut self.common;
|
||||||
if c.last_day_studied != today {
|
if c.last_day_studied != today {
|
||||||
|
@ -80,12 +62,6 @@ impl Deck {
|
||||||
c.last_day_studied = today;
|
c.last_day_studied = today;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl Deck {
|
|
||||||
pub(crate) fn is_filtered(&self) -> bool {
|
|
||||||
matches!(self.kind, DeckKind::Filtered(_))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns deck config ID if deck is a normal deck.
|
/// Returns deck config ID if deck is a normal deck.
|
||||||
pub(crate) fn config_id(&self) -> Option<DeckConfID> {
|
pub(crate) fn config_id(&self) -> Option<DeckConfID> {
|
||||||
|
@ -434,23 +410,6 @@ impl Collection {
|
||||||
self.remove_cards_and_orphaned_notes(&cids)
|
self.remove_cards_and_orphaned_notes(&cids)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn return_all_cards_in_filtered_deck(&mut self, did: DeckID) -> Result<()> {
|
|
||||||
let cids = self.storage.all_cards_in_single_deck(did)?;
|
|
||||||
self.return_cards_to_home_deck(&cids)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn return_cards_to_home_deck(&mut self, cids: &[CardID]) -> Result<()> {
|
|
||||||
let sched = self.sched_ver();
|
|
||||||
for cid in cids {
|
|
||||||
if let Some(mut card) = self.storage.get_card(*cid)? {
|
|
||||||
// fixme: undo
|
|
||||||
card.return_home(sched);
|
|
||||||
self.storage.update_card(&card)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_all_deck_names(&self, skip_empty_default: bool) -> Result<Vec<(DeckID, String)>> {
|
pub fn get_all_deck_names(&self, skip_empty_default: bool) -> Result<Vec<(DeckID, String)>> {
|
||||||
if skip_empty_default && self.default_deck_is_empty()? {
|
if skip_empty_default && self.default_deck_is_empty()? {
|
||||||
Ok(self
|
Ok(self
|
||||||
|
|
|
@ -7,6 +7,7 @@ use crate::{
|
||||||
collection::Collection,
|
collection::Collection,
|
||||||
config::SchedulerVersion,
|
config::SchedulerVersion,
|
||||||
err::Result,
|
err::Result,
|
||||||
|
search::SortMode,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::cutoff::SchedTimingToday;
|
use super::cutoff::SchedTimingToday;
|
||||||
|
@ -59,7 +60,7 @@ impl Collection {
|
||||||
/// Unbury cards from the previous day.
|
/// Unbury cards from the previous day.
|
||||||
/// Done automatically, and does not mark the cards as modified.
|
/// Done automatically, and does not mark the cards as modified.
|
||||||
fn unbury_on_day_rollover(&mut self) -> Result<()> {
|
fn unbury_on_day_rollover(&mut self) -> Result<()> {
|
||||||
self.search_cards_into_table("is:buried")?;
|
self.search_cards_into_table("is:buried", SortMode::NoOrder)?;
|
||||||
self.storage.for_each_card_in_search(|mut card| {
|
self.storage.for_each_card_in_search(|mut card| {
|
||||||
card.restore_queue_after_bury_or_suspend();
|
card.restore_queue_after_bury_or_suspend();
|
||||||
self.storage.update_card(&card)
|
self.storage.update_card(&card)
|
||||||
|
@ -94,7 +95,7 @@ impl Collection {
|
||||||
UnburyDeckMode::SchedOnly => "is:buried-sibling",
|
UnburyDeckMode::SchedOnly => "is:buried-sibling",
|
||||||
};
|
};
|
||||||
self.transact(None, |col| {
|
self.transact(None, |col| {
|
||||||
col.search_cards_into_table(&format!("deck:current {}", search))?;
|
col.search_cards_into_table(&format!("deck:current {}", search), SortMode::NoOrder)?;
|
||||||
col.unsuspend_or_unbury_searched_cards()
|
col.unsuspend_or_unbury_searched_cards()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -125,7 +126,7 @@ impl Collection {
|
||||||
};
|
};
|
||||||
if card.queue != desired_queue {
|
if card.queue != desired_queue {
|
||||||
if sched == SchedulerVersion::V1 {
|
if sched == SchedulerVersion::V1 {
|
||||||
card.return_home(sched);
|
card.remove_from_filtered_deck(sched);
|
||||||
card.remove_from_learning();
|
card.remove_from_learning();
|
||||||
}
|
}
|
||||||
card.queue = desired_queue;
|
card.queue = desired_queue;
|
||||||
|
|
|
@ -63,20 +63,7 @@ impl Collection {
|
||||||
let writer = SqlWriter::new(self);
|
let writer = SqlWriter::new(self);
|
||||||
|
|
||||||
let (mut sql, args) = writer.build_cards_query(&top_node, mode.required_table())?;
|
let (mut sql, args) = writer.build_cards_query(&top_node, mode.required_table())?;
|
||||||
|
self.add_order(&mut sql, mode)?;
|
||||||
match mode {
|
|
||||||
SortMode::NoOrder => (),
|
|
||||||
SortMode::FromConfig => unreachable!(),
|
|
||||||
SortMode::Builtin { kind, reverse } => {
|
|
||||||
prepare_sort(self, kind)?;
|
|
||||||
sql.push_str(" order by ");
|
|
||||||
write_order(&mut sql, kind, reverse)?;
|
|
||||||
}
|
|
||||||
SortMode::Custom(order_clause) => {
|
|
||||||
sql.push_str(" order by ");
|
|
||||||
sql.push_str(&order_clause);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut stmt = self.storage.db.prepare(&sql)?;
|
let mut stmt = self.storage.db.prepare(&sql)?;
|
||||||
let ids: Vec<_> = stmt
|
let ids: Vec<_> = stmt
|
||||||
|
@ -86,13 +73,32 @@ impl Collection {
|
||||||
Ok(ids)
|
Ok(ids)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn add_order(&mut self, sql: &mut String, mode: SortMode) -> Result<()> {
|
||||||
|
match mode {
|
||||||
|
SortMode::NoOrder => (),
|
||||||
|
SortMode::FromConfig => unreachable!(),
|
||||||
|
SortMode::Builtin { kind, reverse } => {
|
||||||
|
prepare_sort(self, kind)?;
|
||||||
|
sql.push_str(" order by ");
|
||||||
|
write_order(sql, kind, reverse)?;
|
||||||
|
}
|
||||||
|
SortMode::Custom(order_clause) => {
|
||||||
|
sql.push_str(" order by ");
|
||||||
|
sql.push_str(&order_clause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Place the matched card ids into a temporary 'search_cids' table
|
/// Place the matched card ids into a temporary 'search_cids' table
|
||||||
/// instead of returning them. Use clear_searched_cards() to remove it.
|
/// instead of returning them. Use clear_searched_cards() to remove it.
|
||||||
pub(crate) fn search_cards_into_table(&mut self, search: &str) -> Result<()> {
|
pub(crate) fn search_cards_into_table(&mut self, search: &str, mode: SortMode) -> Result<()> {
|
||||||
let top_node = Node::Group(parse(search)?);
|
let top_node = Node::Group(parse(search)?);
|
||||||
let writer = SqlWriter::new(self);
|
let writer = SqlWriter::new(self);
|
||||||
|
|
||||||
let (sql, args) = writer.build_cards_query(&top_node, RequiredTable::Cards)?;
|
let (mut sql, args) = writer.build_cards_query(&top_node, mode.required_table())?;
|
||||||
|
self.add_order(&mut sql, mode)?;
|
||||||
|
|
||||||
self.storage
|
self.storage
|
||||||
.db
|
.db
|
||||||
.execute_batch(include_str!("search_cids_setup.sql"))?;
|
.execute_batch(include_str!("search_cids_setup.sql"))?;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// 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
|
||||||
|
|
||||||
use crate::{backend_proto as pb, prelude::*, revlog::RevlogEntry};
|
use crate::{backend_proto as pb, prelude::*, revlog::RevlogEntry, search::SortMode};
|
||||||
|
|
||||||
impl Collection {
|
impl Collection {
|
||||||
pub(crate) fn graph_data_for_search(
|
pub(crate) fn graph_data_for_search(
|
||||||
|
@ -9,7 +9,7 @@ impl Collection {
|
||||||
search: &str,
|
search: &str,
|
||||||
days: u32,
|
days: u32,
|
||||||
) -> Result<pb::GraphsOut> {
|
) -> Result<pb::GraphsOut> {
|
||||||
self.search_cards_into_table(search)?;
|
self.search_cards_into_table(search, SortMode::NoOrder)?;
|
||||||
let all = search.trim().is_empty();
|
let all = search.trim().is_empty();
|
||||||
self.graph_data(all, days)
|
self.graph_data(all, days)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue