diff --git a/CONTRIBUTORS b/CONTRIBUTORS
index a25e4a6f3..b0ab64c5f 100644
--- a/CONTRIBUTORS
+++ b/CONTRIBUTORS
@@ -13,6 +13,16 @@ licensed under the BSD 3 clause license. If any pull request you make contains
code that you don't own the copyright to, you agree to make that clear when
submitting the request.
+When submitting a pull request, GitHub Actions will check that the Git email you
+are submitting from matches the one you used to edit this file. A common issue
+is adding yourself to this file using the username on your computer, but then
+using GitHub to rebase or edit a pull request online. This will result in your
+Git email becoming something like user@noreply.github.com. To prevent the
+automatic check from failing, you can edit this file again using GitHub's online
+editor, making a trivial edit like adding a space after your name, and then pull
+requests will work regardless of whether you create them using your computer or
+GitHub's online interface.
+
For users who previously confirmed the license of their contributions on the
support site, it would be great if you could add your name below as well.
@@ -48,6 +58,7 @@ abdo
aplaice
phwoo
Soren Bjornstad
+Aleksa Sarai
Jakub Kaczmarzyk
********************
diff --git a/README.development b/README.development
index b38a515e3..76083c605 100644
--- a/README.development
+++ b/README.development
@@ -26,7 +26,8 @@ $ pyenv/bin/python -c 'import aqt; aqt.run()'
Building from source
--------------------
-To start, make sure you have the following installed:
+You will need the following dependencies. Some OS-specific tips on installing
+them are further down this document.
- Python 3.7+
- portaudio
@@ -44,14 +45,14 @@ To start, make sure you have the following installed:
- git
- curl
-The build scripts assume a UNIX-like environment, so on Windows you will
-need to use WSL or Cygwin to use them.
-
Once you've installed the above components, execute ./run in this repo,
which will build the subcomponents, and start Anki. Any arguments included
on the command line will be passed on to Anki. The first run will take
quite a while to download and build everything - please be patient.
+Don't name the Git checkout ~/Anki or ~/Documents/Anki, as those folders
+were used on old Anki versions and will be automatically moved.
+
Before contributing code, please read README.contributing.
If you'd like to contribute translations, please see the translations section
@@ -67,6 +68,10 @@ Subcomponents
- proto contains the interface used to communicate between different
languages.
+The pyenv folder is created when running make for the first time.
+It is a Python virtual environment that contains Anki's libraries
+and all the required dependencies.
+
Makefile
--------------
@@ -99,7 +104,7 @@ Install Python 3.7+ if it's not installed.
Install other dependencies:
```
-sudo apt install portaudio19-dev mpv lame npm rsync gcc gettext git curl
+sudo apt install portaudio19-dev mpv lame npm rsync gcc gettext git curl python3-dev python3-venv libxcb-xinerama0
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env
wget https://github.com/protocolbuffers/protobuf/releases/download/v3.11.4/protoc-3.11.4-linux-x86_64.zip
@@ -120,6 +125,9 @@ $ brew link gettext --force
Windows users (using Visual Studio)
----------
+The build scripts assume a UNIX-like environment, so on Windows you will
+need to use WSL or Cygwin to use them.
+
User-contributed instructions for building using Cygwin:
1. Download and install Cygwin and put its `/bin/` directory on your system path (This PC > Properties > Advancded system settings > Environment Variables > double-click Path > New).
diff --git a/proto/backend.proto b/proto/backend.proto
index ac5340e32..b438cca38 100644
--- a/proto/backend.proto
+++ b/proto/backend.proto
@@ -95,7 +95,8 @@ service BackendService {
rpc LocalMinutesWest (Int64) returns (Int32);
rpc SetLocalMinutesWest (Int32) returns (Empty);
rpc SchedTimingToday (Empty) returns (SchedTimingTodayOut);
- rpc StudiedToday (StudiedTodayIn) returns (String);
+ rpc StudiedToday (Empty) returns (String);
+ rpc StudiedTodayMessage (StudiedTodayMessageIn) returns (String);
rpc UpdateStats (UpdateStatsIn) returns (Empty);
rpc ExtendLimits (ExtendLimitsIn) returns (Empty);
rpc CountsForDeckToday (DeckID) returns (CountsForDeckTodayOut);
@@ -103,6 +104,12 @@ service BackendService {
rpc RestoreBuriedAndSuspendedCards (CardIDs) returns (Empty);
rpc UnburyCardsInCurrentDeck (UnburyCardsInCurrentDeckIn) returns (Empty);
rpc BuryOrSuspendCards (BuryOrSuspendCardsIn) returns (Empty);
+ rpc EmptyFilteredDeck (DeckID) returns (Empty);
+ rpc RebuildFilteredDeck (DeckID) returns (UInt32);
+ rpc ScheduleCardsAsReviews (ScheduleCardsAsReviewsIn) returns (Empty);
+ rpc ScheduleCardsAsNew (CardIDs) returns (Empty);
+ rpc SortCards (SortCardsIn) returns (Empty);
+ rpc SortDeck (SortDeckIn) returns (Empty);
// stats
@@ -143,6 +150,7 @@ service BackendService {
rpc UpdateCard (Card) returns (Empty);
rpc AddCard (Card) returns (CardID);
rpc RemoveCards (RemoveCardsIn) returns (Empty);
+ rpc SetDeck (SetDeckIn) returns (Empty);
// notes
@@ -682,7 +690,7 @@ message FormatTimespanIn {
Context context = 2;
}
-message StudiedTodayIn {
+message StudiedTodayMessageIn {
uint32 cards = 1;
double seconds = 2;
}
@@ -1013,6 +1021,7 @@ message RevlogEntry {
REVIEW = 1;
RELEARNING = 2;
EARLY_REVIEW = 3;
+ MANUAL = 4;
}
int64 id = 1;
int64 cid = 2;
@@ -1054,3 +1063,27 @@ message BuryOrSuspendCardsIn {
repeated int64 card_ids = 1;
Mode mode = 2;
}
+
+message ScheduleCardsAsReviewsIn {
+ repeated int64 card_ids = 1;
+ uint32 min_interval = 2;
+ uint32 max_interval = 3;
+}
+
+message SortCardsIn {
+ repeated int64 card_ids = 1;
+ uint32 starting_from = 2;
+ uint32 step_size = 3;
+ bool randomize = 4;
+ bool shift_existing = 5;
+}
+
+message SortDeckIn {
+ int64 deck_id = 1;
+ bool randomize = 2;
+}
+
+message SetDeckIn {
+ repeated int64 card_ids = 1;
+ int64 deck_id = 2;
+}
diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py
index 320a6565b..cf39340e8 100644
--- a/pylib/anki/collection.py
+++ b/pylib/anki/collection.py
@@ -384,6 +384,9 @@ class Collection:
"You probably want .remove_notes_by_card() instead."
self.backend.remove_cards(card_ids=card_ids)
+ def set_deck(self, card_ids: List[int], deck_id: int) -> None:
+ self.backend.set_deck(card_ids=card_ids, deck_id=deck_id)
+
# legacy
def remCards(self, ids: List[int], notes: bool = True) -> None:
@@ -516,6 +519,9 @@ table.review-log {{ {revlog_style} }}
return style + self.backend.card_stats(card_id)
+ def studied_today(self) -> str:
+ return self.backend.studied_today()
+
# legacy
def cardStats(self, card: Card) -> str:
diff --git a/pylib/anki/decks.py b/pylib/anki/decks.py
index 2da3cbe9c..a6284950e 100644
--- a/pylib/anki/decks.py
+++ b/pylib/anki/decks.py
@@ -556,7 +556,7 @@ class DeckManager:
# Dynamic decks
##########################################################################
- def newDyn(self, name: str) -> int:
+ def new_filtered(self, name: str) -> int:
"Return a new dynamic deck and set it as the current deck."
did = self.id(name, type=1)
self.select(did)
@@ -565,3 +565,7 @@ class DeckManager:
# 1 for dyn, 0 for standard
def isDyn(self, did: Union[int, str]) -> int:
return self.get(did)["dyn"]
+
+ # legacy
+
+ newDyn = new_filtered
diff --git a/pylib/anki/sched.py b/pylib/anki/sched.py
index 924972b34..333c4d8e0 100644
--- a/pylib/anki/sched.py
+++ b/pylib/anki/sched.py
@@ -6,7 +6,7 @@ from __future__ import annotations
import random
import time
from heapq import *
-from typing import Any, List, Optional, Sequence, Tuple, Union
+from typing import Any, List, Optional, Tuple, Union
import anki
from anki import hooks
@@ -599,77 +599,9 @@ did = ? and queue = {QUEUE_TYPE_REV} and due <= ? limit ?""",
idealIvl = self._fuzzedIvl(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:
assert card.odid and card.type == CARD_TYPE_REV
assert card.factor
diff --git a/pylib/anki/schedv2.py b/pylib/anki/schedv2.py
index 7ca006b83..1b3660a7d 100644
--- a/pylib/anki/schedv2.py
+++ b/pylib/anki/schedv2.py
@@ -15,7 +15,6 @@ from typing import (
List,
Optional,
Sequence,
- Set,
Tuple,
Union,
)
@@ -25,7 +24,7 @@ import anki.backend_pb2 as pb
from anki import hooks
from anki.cards import Card
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.notes import Note
from anki.rsbackend import (
@@ -1062,117 +1061,14 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l
return ivl
- # Dynamic deck handling
+ # Filtered deck handling
##########################################################################
- _restoreQueueWhenEmptyingSnippet = f"""
-queue = (case when queue < 0 then queue
- when type in (1,{CARD_TYPE_RELEARNING}) then
- (case when (case when odue then odue else due end) > 1000000000 then 1 else
- {QUEUE_TYPE_DAY_LEARN_RELEARN} end)
-else
- type
-end)
-"""
+ def rebuild_filtered_deck(self, deck_id: int) -> int:
+ return self.col.backend.rebuild_filtered_deck(deck_id)
- def rebuildDyn(self, did: Optional[int] = None) -> Optional[int]:
- "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)
- cnt = self._fillDyn(deck)
- if not cnt:
- return None
- # and change to our new deck
- self.col.decks.select(did)
- return cnt
-
- 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:
- if not lim:
- lim = "did = %s" % did
- self.col.log(self.col.db.list("select id from cards where %s" % lim))
-
- self.col.db.execute(
- """
-update cards set did = odid, %s,
-due = (case when odue>0 then odue else due end), odue = 0, odid = 0, usn = ? where %s"""
- % (self._restoreQueueWhenEmptyingSnippet, lim),
- self.col.usn(),
- )
-
- def remFromDyn(self, cids: List[int]) -> None:
- 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 empty_filtered_deck(self, deck_id: int) -> None:
+ self.col.backend.empty_filtered_deck(deck_id)
def _removeFromFiltered(self, card: Card) -> None:
if card.odid:
@@ -1195,6 +1091,42 @@ where id = ?
else:
card.queue = card.type
+ # legacy
+
+ def rebuildDyn(self, did: Optional[int] = None) -> Optional[int]:
+ did = did or self.col.decks.selected()
+ count = self.rebuild_filtered_deck(did) or None
+ if not count:
+ return None
+ # and change to our new deck
+ self.col.decks.select(did)
+ return count
+
+ def emptyDyn(self, did: Optional[int], lim: Optional[str] = None) -> None:
+ if lim is None:
+ self.empty_filtered_deck(did)
+ return
+
+ queue = f"""
+queue = (case when queue < 0 then queue
+ when type in (1,{CARD_TYPE_RELEARNING}) then
+ (case when (case when odue then odue else due end) > 1000000000 then 1 else
+ {QUEUE_TYPE_DAY_LEARN_RELEARN} end)
+else
+ type
+end)
+"""
+ self.col.db.execute(
+ """
+update cards set did = odid, %s,
+due = (case when odue>0 then odue else due end), odue = 0, odid = 0, usn = ? where %s"""
+ % (queue, lim),
+ self.col.usn(),
+ )
+
+ def remFromDyn(self, cids: List[int]) -> None:
+ self.emptyDyn(None, "id in %s and odid" % ids2str(cids))
+
# Leeches
##########################################################################
@@ -1474,47 +1406,17 @@ and (queue={QUEUE_TYPE_NEW} or (queue={QUEUE_TYPE_REV} and due<=?))""",
# Resetting
##########################################################################
- def forgetCards(self, ids: List[int]) -> None:
+ def schedule_cards_as_new(self, card_ids: List[int]) -> None:
"Put cards at the end of the new queue."
- self.remFromDyn(ids)
- self.col.db.execute(
- f"update cards set type={CARD_TYPE_NEW},queue={QUEUE_TYPE_NEW},ivl=0,due=0,odue=0,factor=?"
- " where id in " + ids2str(ids),
- STARTING_FACTOR,
- )
- pmax = (
- self.col.db.scalar(f"select max(due) from cards where type={CARD_TYPE_NEW}")
- or 0
- )
- # takes care of mod + usn
- self.sortCards(ids, start=pmax + 1)
- self.col.log(ids)
+ self.col.backend.schedule_cards_as_new(card_ids)
- def reschedCards(self, ids: List[int], imin: int, imax: int) -> None:
- "Put cards in review queue with a new interval in days (min, max)."
- d = []
- t = self.today
- mod = intTime()
- for id in ids:
- r = random.randint(imin, imax)
- d.append(
- (
- max(1, r),
- r + t,
- self.col.usn(),
- mod,
- STARTING_FACTOR,
- id,
- )
- )
- self.remFromDyn(ids)
- self.col.db.executemany(
- f"""
-update cards set type={CARD_TYPE_REV},queue={QUEUE_TYPE_REV},ivl=?,due=?,odue=0,
-usn=?,mod=?,factor=? where id=?""",
- d,
+ def schedule_cards_as_reviews(
+ self, card_ids: List[int], min_interval: int, max_interval: int
+ ) -> None:
+ "Make cards review cards, with a new interval randomly selected from range."
+ self.col.backend.schedule_cards_as_reviews(
+ card_ids=card_ids, min_interval=min_interval, max_interval=max_interval
)
- self.col.log(ids)
def resetCards(self, ids: List[int]) -> None:
"Completely reset cards for export."
@@ -1533,6 +1435,11 @@ usn=?,mod=?,factor=? where id=?""",
self.forgetCards(nonNew)
self.col.log(ids)
+ # legacy
+
+ forgetCards = schedule_cards_as_new
+ reschedCards = schedule_cards_as_reviews
+
# Repositioning new cards
##########################################################################
@@ -1544,60 +1451,19 @@ usn=?,mod=?,factor=? where id=?""",
shuffle: bool = False,
shift: bool = False,
) -> None:
- scids = ids2str(cids)
- now = intTime()
- nids = []
- nidsSet: Set[int] = set()
- for id in cids:
- nid = self.col.db.scalar("select nid from cards where id = ?", id)
- if nid not in nidsSet:
- nids.append(nid)
- nidsSet.add(nid)
- if not nids:
- # no new cards
- return
- # determine nid ordering
- due = {}
- if shuffle:
- random.shuffle(nids)
- for c, nid in enumerate(nids):
- due[nid] = start + c * step
- # pylint: disable=undefined-loop-variable
- high = start + c * step
- # shift?
- if shift:
- low = self.col.db.scalar(
- f"select min(due) from cards where due >= ? and type = {CARD_TYPE_NEW} "
- "and id not in %s" % scids,
- start,
- )
- if low is not None:
- shiftby = high - low + 1
- self.col.db.execute(
- f"""
-update cards set mod=?, usn=?, due=due+? where id not in %s
-and due >= ? and queue = {QUEUE_TYPE_NEW}"""
- % scids,
- now,
- self.col.usn(),
- shiftby,
- low,
- )
- # reorder cards
- d = []
- for id, nid in self.col.db.execute(
- f"select id, nid from cards where type = {CARD_TYPE_NEW} and id in " + scids
- ):
- d.append((due[nid], now, self.col.usn(), id))
- self.col.db.executemany("update cards set due=?,mod=?,usn=? where id = ?", d)
+ self.col.backend.sort_cards(
+ card_ids=cids,
+ starting_from=start,
+ step_size=step,
+ randomize=shuffle,
+ shift_existing=shift,
+ )
def randomizeCards(self, did: int) -> None:
- cids = self.col.db.list("select id from cards where did = ?", did)
- self.sortCards(cids, shuffle=True)
+ self.col.backend.sort_deck(deck_id=did, randomize=True)
def orderCards(self, did: int) -> None:
- cids = self.col.db.list("select id from cards where did = ? order by nid", did)
- self.sortCards(cids)
+ self.col.backend.sort_deck(deck_id=did, randomize=False)
def resortConf(self, conf) -> None:
for did in self.col.decks.didsForConf(conf):
diff --git a/pylib/anki/stats.py b/pylib/anki/stats.py
index e3f34fe3a..aedf641ec 100644
--- a/pylib/anki/stats.py
+++ b/pylib/anki/stats.py
@@ -145,7 +145,9 @@ from revlog where id > ? """
return "" + str(s) + ""
if cards:
- b += self.col.backend.studied_today(cards=cards, seconds=float(thetime))
+ b += self.col.backend.studied_today_message(
+ cards=cards, seconds=float(thetime)
+ )
# again/pass count
b += "
" + _("Again count: %s") % bold(failed)
if cards:
diff --git a/pylib/tests/test_decks.py b/pylib/tests/test_decks.py
index 4dbe61551..59fb8e130 100644
--- a/pylib/tests/test_decks.py
+++ b/pylib/tests/test_decks.py
@@ -84,7 +84,7 @@ def test_rename():
for n in "yo", "yo::two", "yo::two::three":
assert n in names
# over filtered
- filteredId = col.decks.newDyn("filtered")
+ filteredId = col.decks.new_filtered("filtered")
filtered = col.decks.get(filteredId)
childId = col.decks.id("child")
child = col.decks.get(childId)
diff --git a/pylib/tests/test_schedv1.py b/pylib/tests/test_schedv1.py
index 025ec55de..fe5452cb1 100644
--- a/pylib/tests/test_schedv1.py
+++ b/pylib/tests/test_schedv1.py
@@ -545,8 +545,8 @@ def test_suspend():
# should cope with cards in cram decks
c.due = 1
c.flush()
- col.decks.newDyn("tmp")
- col.sched.rebuildDyn()
+ did = col.decks.new_filtered("tmp")
+ col.sched.rebuild_filtered_deck(did)
c.load()
assert c.due != 1
assert c.did != 1
@@ -575,8 +575,8 @@ def test_cram():
assert col.sched.counts() == (0, 0, 0)
cardcopy = copy.copy(c)
# create a dynamic deck and refresh it
- did = col.decks.newDyn("Cram")
- col.sched.rebuildDyn(did)
+ did = col.decks.new_filtered("Cram")
+ col.sched.rebuild_filtered_deck(did)
col.reset()
# should appear as new in the deck list
assert sorted(col.sched.deck_due_tree().children)[0].new_count == 1
@@ -616,7 +616,7 @@ def test_cram():
# and it will have moved back to the previous deck
assert c.did == 1
# cram the deck again
- col.sched.rebuildDyn(did)
+ col.sched.rebuild_filtered_deck(did)
col.reset()
c = col.sched.getCard()
# check ivls again - passing should be idempotent
@@ -646,8 +646,8 @@ def test_cram():
col.reset()
assert col.sched.counts() == (0, 0, 1)
# cram again
- did = col.decks.newDyn("Cram")
- col.sched.rebuildDyn(did)
+ did = col.decks.new_filtered("Cram")
+ col.sched.rebuild_filtered_deck(did)
col.reset()
assert col.sched.counts() == (0, 0, 1)
c.load()
@@ -673,8 +673,8 @@ def test_cram_rem():
note["Front"] = "one"
col.addNote(note)
oldDue = note.cards()[0].due
- did = col.decks.newDyn("Cram")
- col.sched.rebuildDyn(did)
+ did = col.decks.new_filtered("Cram")
+ col.sched.rebuild_filtered_deck(did)
col.reset()
c = col.sched.getCard()
col.sched.answerCard(c, 2)
@@ -682,7 +682,7 @@ def test_cram_rem():
assert c.type == CARD_TYPE_LRN and c.queue == QUEUE_TYPE_LRN
assert c.due != oldDue
# if we terminate cramming prematurely it should be set back to new
- col.sched.emptyDyn(did)
+ col.sched.empty_filtered_deck(did)
c.load()
assert c.type == CARD_TYPE_NEW and c.queue == QUEUE_TYPE_NEW
assert c.due == oldDue
@@ -695,11 +695,11 @@ def test_cram_resched():
note["Front"] = "one"
col.addNote(note)
# cram deck
- did = col.decks.newDyn("Cram")
+ did = col.decks.new_filtered("Cram")
cram = col.decks.get(did)
cram["resched"] = False
col.decks.save(cram)
- col.sched.rebuildDyn(did)
+ col.sched.rebuild_filtered_deck(did)
col.reset()
# graduate should return it to new
c = col.sched.getCard()
@@ -718,7 +718,7 @@ def test_cram_resched():
c.factor = STARTING_FACTOR
c.flush()
cardcopy = copy.copy(c)
- col.sched.rebuildDyn(did)
+ col.sched.rebuild_filtered_deck(did)
col.reset()
c = col.sched.getCard()
assert ni(c, 1) == 600
@@ -730,23 +730,23 @@ def test_cram_resched():
# check failure too
c = cardcopy
c.flush()
- col.sched.rebuildDyn(did)
+ col.sched.rebuild_filtered_deck(did)
col.reset()
c = col.sched.getCard()
col.sched.answerCard(c, 1)
- col.sched.emptyDyn(did)
+ col.sched.empty_filtered_deck(did)
c.load()
assert c.ivl == 100
assert c.due == col.sched.today + 25
# fail+grad early
c = cardcopy
c.flush()
- col.sched.rebuildDyn(did)
+ col.sched.rebuild_filtered_deck(did)
col.reset()
c = col.sched.getCard()
col.sched.answerCard(c, 1)
col.sched.answerCard(c, 3)
- col.sched.emptyDyn(did)
+ col.sched.empty_filtered_deck(did)
c.load()
assert c.ivl == 100
assert c.due == col.sched.today + 25
@@ -754,11 +754,11 @@ def test_cram_resched():
c = cardcopy
c.due = -25
c.flush()
- col.sched.rebuildDyn(did)
+ col.sched.rebuild_filtered_deck(did)
col.reset()
c = col.sched.getCard()
col.sched.answerCard(c, 3)
- col.sched.emptyDyn(did)
+ col.sched.empty_filtered_deck(did)
c.load()
assert c.ivl == 100
assert c.due == -25
@@ -766,11 +766,11 @@ def test_cram_resched():
c = cardcopy
c.due = -25
c.flush()
- col.sched.rebuildDyn(did)
+ col.sched.rebuild_filtered_deck(did)
col.reset()
c = col.sched.getCard()
col.sched.answerCard(c, 1)
- col.sched.emptyDyn(did)
+ col.sched.empty_filtered_deck(did)
c.load()
assert c.ivl == 100
assert c.due == -25
@@ -778,7 +778,7 @@ def test_cram_resched():
c = cardcopy
c.due = -25
c.flush()
- col.sched.rebuildDyn(did)
+ col.sched.rebuild_filtered_deck(did)
col.reset()
c = col.sched.getCard()
col.sched.answerCard(c, 1)
@@ -789,7 +789,7 @@ def test_cram_resched():
# lapsed card pulled into cram
# col.sched._cardConf(c)['lapse']['mult']=0.5
# col.sched.answerCard(c, 1)
- # col.sched.rebuildDyn(did)
+ # col.sched.rebuild_filtered_deck(did)
# col.reset()
# c = col.sched.getCard()
# col.sched.answerCard(c, 2)
diff --git a/pylib/tests/test_schedv2.py b/pylib/tests/test_schedv2.py
index 6e928ae7b..f09a63094 100644
--- a/pylib/tests/test_schedv2.py
+++ b/pylib/tests/test_schedv2.py
@@ -668,8 +668,8 @@ def test_suspend():
# should cope with cards in cram decks
c.due = 1
c.flush()
- col.decks.newDyn("tmp")
- col.sched.rebuildDyn()
+ did = col.decks.new_filtered("tmp")
+ col.sched.rebuild_filtered_deck(did)
c.load()
assert c.due != 1
assert c.did != 1
@@ -698,8 +698,8 @@ def test_filt_reviewing_early_normal():
col.reset()
assert col.sched.counts() == (0, 0, 0)
# create a dynamic deck and refresh it
- did = col.decks.newDyn("Cram")
- col.sched.rebuildDyn(did)
+ did = col.decks.new_filtered("Cram")
+ col.sched.rebuild_filtered_deck(did)
col.reset()
# should appear as normal in the deck list
assert sorted(col.sched.deck_due_tree().children)[0].review_count == 1
@@ -727,7 +727,7 @@ def test_filt_reviewing_early_normal():
c.ivl = 100
c.due = col.sched.today + 75
c.flush()
- col.sched.rebuildDyn(did)
+ col.sched.rebuild_filtered_deck(did)
col.reset()
c = col.sched.getCard()
@@ -758,8 +758,8 @@ def test_filt_keep_lrn_state():
assert c.type == CARD_TYPE_LRN and c.queue == QUEUE_TYPE_LRN
# create a dynamic deck and refresh it
- did = col.decks.newDyn("Cram")
- col.sched.rebuildDyn(did)
+ did = col.decks.new_filtered("Cram")
+ col.sched.rebuild_filtered_deck(did)
col.reset()
# card should still be in learning state
@@ -773,7 +773,7 @@ def test_filt_keep_lrn_state():
assert c.due - intTime() > 60 * 60
# emptying the deck preserves learning state
- col.sched.emptyDyn(did)
+ col.sched.empty_filtered_deck(did)
c.load()
assert c.type == CARD_TYPE_LRN and c.queue == QUEUE_TYPE_LRN
assert c.left == 1001
@@ -792,11 +792,11 @@ def test_preview():
note2["Front"] = "two"
col.addNote(note2)
# cram deck
- did = col.decks.newDyn("Cram")
+ did = col.decks.new_filtered("Cram")
cram = col.decks.get(did)
cram["resched"] = False
col.decks.save(cram)
- col.sched.rebuildDyn(did)
+ col.sched.rebuild_filtered_deck(did)
col.reset()
# grab the first card
c = col.sched.getCard()
@@ -823,7 +823,7 @@ def test_preview():
assert c.id == orig.id
# emptying the filtered deck should restore card
- col.sched.emptyDyn(did)
+ col.sched.empty_filtered_deck(did)
c.load()
assert c.queue == QUEUE_TYPE_NEW
assert c.reps == 0
@@ -1253,9 +1253,9 @@ def test_negativeDueFilter():
c.flush()
# into and out of filtered deck
- did = col.decks.newDyn("Cram")
- col.sched.rebuildDyn(did)
- col.sched.emptyDyn(did)
+ did = col.decks.new_filtered("Cram")
+ col.sched.rebuild_filtered_deck(did)
+ col.sched.empty_filtered_deck(did)
col.reset()
c.load()
diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py
index 816b85f42..d36909606 100644
--- a/qt/aqt/browser.py
+++ b/qt/aqt/browser.py
@@ -22,7 +22,7 @@ from anki.models import NoteType
from anki.notes import Note
from anki.rsbackend import TR, DeckTreeNode, InvalidInput
from anki.stats import CardStats
-from anki.utils import htmlToTextLine, ids2str, intTime, isMac, isWin
+from anki.utils import htmlToTextLine, ids2str, isMac, isWin
from aqt import AnkiQt, gui_hooks
from aqt.editor import Editor
from aqt.exporting import ExportDialog
@@ -1601,21 +1601,7 @@ where id in %s"""
return
self.model.beginReset()
self.mw.checkpoint(_("Change Deck"))
- mod = intTime()
- usn = self.col.usn()
- # normal cards
- scids = ids2str(cids)
- # remove any cards from filtered deck first
- self.col.sched.remFromDyn(cids)
- # then move into new deck
- self.col.db.execute(
- """
-update cards set usn=?, mod=?, did=? where id in """
- + scids,
- usn,
- mod,
- did,
- )
+ self.col.set_deck(cids, did)
self.model.endReset()
self.mw.requireReset(reason=ResetReason.BrowserSetDeck, context=self)
diff --git a/qt/aqt/customstudy.py b/qt/aqt/customstudy.py
index 1b5debc8e..bf049575e 100644
--- a/qt/aqt/customstudy.py
+++ b/qt/aqt/customstudy.py
@@ -145,12 +145,12 @@ class CustomStudy(QDialog):
return QDialog.accept(self)
else:
# safe to empty
- self.mw.col.sched.emptyDyn(cur["id"])
+ self.mw.col.sched.empty_filtered_deck(cur["id"])
# reuse; don't delete as it may have children
dyn = cur
self.mw.col.decks.select(cur["id"])
else:
- did = self.mw.col.decks.newDyn(_("Custom Study Session"))
+ did = self.mw.col.decks.new_filtered(_("Custom Study Session"))
dyn = self.mw.col.decks.get(did)
# and then set various options
if i == RADIO_FORGOT:
@@ -186,7 +186,7 @@ class CustomStudy(QDialog):
self.mw.col.decks.save(dyn)
# generate cards
self.created_custom_study = True
- if not self.mw.col.sched.rebuildDyn():
+ if not self.mw.col.sched.rebuild_filtered_deck(dyn["id"]):
return showWarning(_("No cards matched the criteria you provided."))
self.mw.moveToState("overview")
QDialog.accept(self)
diff --git a/qt/aqt/deckbrowser.py b/qt/aqt/deckbrowser.py
index b08eff116..d79aa7c08 100644
--- a/qt/aqt/deckbrowser.py
+++ b/qt/aqt/deckbrowser.py
@@ -138,16 +138,7 @@ class DeckBrowser:
self.web.eval("$(function() { window.scrollTo(0, %d, 'instant'); });" % offset)
def _renderStats(self):
- cards, thetime = self.mw.col.db.first(
- """
-select count(), sum(time)/1000 from revlog
-where id > ?""",
- (self.mw.col.sched.dayCutoff - 86400) * 1000,
- )
- cards = cards or 0
- thetime = thetime or 0
- buf = self.mw.col.backend.studied_today(cards=cards, seconds=float(thetime))
- return buf
+ return self.mw.col.studied_today()
def _renderDeckTree(self, top: DeckTreeNode) -> str:
buf = """
diff --git a/qt/aqt/dyndeckconf.py b/qt/aqt/dyndeckconf.py
index 91c5d89e6..b1c018193 100644
--- a/qt/aqt/dyndeckconf.py
+++ b/qt/aqt/dyndeckconf.py
@@ -122,7 +122,7 @@ class DeckConf(QDialog):
def accept(self):
if not self.saveConf():
return
- if not self.mw.col.sched.rebuildDyn():
+ if not self.mw.col.sched.rebuild_filtered_deck(self.deck["id"]):
if askUser(
_(
"""\
diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py
index 2a980d037..05d57d507 100644
--- a/qt/aqt/editor.py
+++ b/qt/aqt/editor.py
@@ -493,7 +493,9 @@ class Editor:
self.web.eval("setBackgrounds(%s);" % json.dumps(cols))
def showDupes(self):
- contents = html.escape(stripHTMLMedia(self.note.fields[0]))
+ contents = html.escape(
+ stripHTMLMedia(self.note.fields[0]), quote=False
+ ).replace('"', r"\"")
browser = aqt.dialogs.open("Browser", self.mw)
browser.form.searchEdit.lineEdit().setText(
'"dupe:%s,%s"' % (self.note.model()["id"], contents)
@@ -743,7 +745,6 @@ to a cloze type first, via 'Notes>Change Note Type'"""
)
return
if file:
- av_player.play_file(file)
self.addMedia(file)
# Media downloads
@@ -761,7 +762,8 @@ to a cloze type first, via 'Notes>Change Note Type'"""
name = urllib.parse.quote(fname.encode("utf8"))
return '' % name
else:
- return "[sound:%s]" % fname
+ av_player.play_file(fname)
+ return "[sound:%s]" % html.escape(fname, quote=False)
def urlToFile(self, url: str) -> Optional[str]:
l = url.lower()
diff --git a/qt/aqt/main.py b/qt/aqt/main.py
index f9b80ef3a..25be4c4ac 100644
--- a/qt/aqt/main.py
+++ b/qt/aqt/main.py
@@ -1159,7 +1159,7 @@ title="%s" %s>%s""" % (
while self.col.decks.id_for_name(_("Filtered Deck %d") % n):
n += 1
name = _("Filtered Deck %d") % n
- did = self.col.decks.newDyn(name)
+ did = self.col.decks.new_filtered(name)
diag = aqt.dyndeckconf.DeckConf(self, first=True, search=search)
if not diag.ok:
# user cancelled first config
diff --git a/qt/aqt/overview.py b/qt/aqt/overview.py
index 58f49c005..bc02b4e85 100644
--- a/qt/aqt/overview.py
+++ b/qt/aqt/overview.py
@@ -76,10 +76,10 @@ class Overview:
deck = self.mw.col.decks.current()
self.mw.onCram("'deck:%s'" % deck["name"])
elif url == "refresh":
- self.mw.col.sched.rebuildDyn()
+ self.mw.col.sched.rebuild_filtered_deck(self.mw.col.decks.selected())
self.mw.reset()
elif url == "empty":
- self.mw.col.sched.emptyDyn(self.mw.col.decks.selected())
+ self.mw.col.sched.empty_filtered_deck(self.mw.col.decks.selected())
self.mw.reset()
elif url == "decks":
self.mw.moveToState("deckBrowser")
@@ -107,12 +107,12 @@ class Overview:
def onRebuildKey(self):
if self._filteredDeck():
- self.mw.col.sched.rebuildDyn()
+ self.mw.col.sched.rebuild_filtered_deck(self.mw.col.decks.selected())
self.mw.reset()
def onEmptyKey(self):
if self._filteredDeck():
- self.mw.col.sched.emptyDyn(self.mw.col.decks.selected())
+ self.mw.col.sched.empty_filtered_deck(self.mw.col.decks.selected())
self.mw.reset()
def onCustomStudyKey(self):
diff --git a/rslib/Cargo.toml b/rslib/Cargo.toml
index fc3a3f437..8f4d873c1 100644
--- a/rslib/Cargo.toml
+++ b/rslib/Cargo.toml
@@ -28,7 +28,7 @@ serde = "1.0.114"
serde_json = "1.0.56"
tokio = { version = "0.2.21", features = ["fs", "rt-threaded"] }
serde_derive = "1.0.114"
-zip = "0.5.6"
+zip = { version = "0.5.6", default-features = false, features = ["deflate", "time"] }
serde_tuple = "0.5.0"
coarsetime = { git = "https://github.com/ankitects/rust-coarsetime.git", branch="old-mac-compat" }
utime = "0.3.1"
@@ -52,6 +52,8 @@ pin-project = "0.4.22"
async-compression = { version = "0.3.5", features = ["stream", "gzip"] }
askama = "0.10.1"
hyper = "0.13.7"
+once_cell = "1.4.1"
+scopeguard = "1.1.0"
[target.'cfg(target_vendor="apple")'.dependencies.rusqlite]
version = "0.23.1"
diff --git a/rslib/build.rs b/rslib/build.rs
index 9722afc41..acc1ed09a 100644
--- a/rslib/build.rs
+++ b/rslib/build.rs
@@ -1,6 +1,7 @@
use std::fmt::Write;
use std::fs;
use std::path::Path;
+use std::process::Command;
use fluent_syntax::ast::{Entry::Message, ResourceEntry};
use fluent_syntax::parser::parse;
@@ -115,7 +116,7 @@ fn write_method_trait(buf: &mut String, service: &prost_build::Service) {
use prost::Message;
pub type BackendResult = std::result::Result;
pub trait BackendService {
- fn run_command_bytes2_inner(&mut self, method: u32, input: &[u8]) -> std::result::Result, crate::err::AnkiError> {
+ fn run_command_bytes2_inner(&self, method: u32, input: &[u8]) -> std::result::Result, crate::err::AnkiError> {
match method {
"#,
);
@@ -145,7 +146,7 @@ pub trait BackendService {
write!(
buf,
concat!(
- " fn {method_name}(&mut self, input: {input_type}) -> ",
+ " fn {method_name}(&self, input: {input_type}) -> ",
"BackendResult<{output_type}>;\n"
),
method_name = method.name,
@@ -200,15 +201,20 @@ fn main() -> std::io::Result<()> {
fs::write(rust_string_path, rust_string_vec(&idents))?;
// output protobuf generated code
- // we avoid default OUT_DIR for now, as it breaks code completion
- std::env::set_var("OUT_DIR", "src");
println!("cargo:rerun-if-changed=../proto/backend.proto");
-
let mut config = prost_build::Config::new();
- config.service_generator(service_generator());
config
+ // we avoid default OUT_DIR for now, as it breaks code completion
+ .out_dir("src")
+ .service_generator(service_generator())
.compile_protos(&["../proto/backend.proto"], &["../proto"])
.unwrap();
+ // rustfmt the protobuf code
+ let rustfmt = Command::new("rustfmt")
+ .arg(Path::new("src/backend_proto.rs"))
+ .status()
+ .unwrap();
+ assert!(rustfmt.success(), "rustfmt backend_proto.rs failed");
// write the other language ftl files
let mut ftl_lang_dirs = vec!["./ftl/repo/core".to_string()];
diff --git a/rslib/ftl/card-stats.ftl b/rslib/ftl/card-stats.ftl
index 63d78df88..ddaf797d9 100644
--- a/rslib/ftl/card-stats.ftl
+++ b/rslib/ftl/card-stats.ftl
@@ -21,3 +21,5 @@ card-stats-review-log-type-learn = Learn
card-stats-review-log-type-review = Review
card-stats-review-log-type-relearn = Relearn
card-stats-review-log-type-filtered = Filtered
+card-stats-review-log-type-manual = Manual
+
diff --git a/rslib/rust-toolchain b/rslib/rust-toolchain
index 22bde8163..2bf5ad044 100644
--- a/rslib/rust-toolchain
+++ b/rslib/rust-toolchain
@@ -1 +1 @@
-nightly-2020-06-25
+stable
diff --git a/rslib/rustfmt.toml b/rslib/rustfmt.toml
deleted file mode 100644
index bd0ab67a8..000000000
--- a/rslib/rustfmt.toml
+++ /dev/null
@@ -1 +0,0 @@
-ignore = ["backend_proto.rs"]
diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs
index 72be90733..698d06135 100644
--- a/rslib/src/backend/mod.rs
+++ b/rslib/src/backend/mod.rs
@@ -31,8 +31,9 @@ use crate::{
RenderCardOutput,
},
sched::cutoff::local_minutes_west_for_stamp,
- sched::timespan::{answer_button_time, studied_today, time_span},
+ sched::timespan::{answer_button_time, time_span},
search::SortMode,
+ stats::studied_today,
sync::{
get_remote_sync_meta, sync_abort, sync_login, FullSyncProgress, NormalSyncProgress,
SyncActionRequired, SyncAuth, SyncMeta, SyncOutput, SyncStage,
@@ -43,11 +44,13 @@ use crate::{
types::Usn,
};
use fluent::FluentValue;
-use futures::future::{AbortHandle, Abortable};
+use futures::future::{AbortHandle, AbortRegistration, Abortable};
use log::error;
+use once_cell::sync::OnceCell;
use pb::{sync_status_out, BackendService};
use prost::Message;
use serde_json::Value as JsonValue;
+use slog::warn;
use std::collections::{HashMap, HashSet};
use std::convert::TryFrom;
use std::{
@@ -84,13 +87,16 @@ struct ProgressState {
last_progress: Option
{/if}
-
- {@html customStudyMsg}
-
+ {#if !info.isFilteredDeck}
+
+ {@html customStudyMsg}
+
+ {/if}
{/if}
diff --git a/ts/src/stats/reviews.ts b/ts/src/stats/reviews.ts
index b6a41d9df..a33b878a8 100644
--- a/ts/src/stats/reviews.ts
+++ b/ts/src/stats/reviews.ts
@@ -47,6 +47,10 @@ export function gatherData(data: pb.BackendProto.GraphsOut): GraphData {
const empty = { mature: 0, young: 0, learn: 0, relearn: 0, early: 0 };
for (const review of data.revlog as pb.BackendProto.RevlogEntry[]) {
+ if (review.reviewKind == ReviewKind.MANUAL) {
+ // don't count days with only manual scheduling
+ continue;
+ }
const day = Math.ceil(
((review.id as number) / 1000 - data.nextDayAtSecs) / 86400
);
diff --git a/ts/src/stats/today.ts b/ts/src/stats/today.ts
index 1c861d4c8..db77073bc 100644
--- a/ts/src/stats/today.ts
+++ b/ts/src/stats/today.ts
@@ -1,7 +1,7 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-import pb from "../backend/proto";
+import pb, { BackendProto } from "../backend/proto";
import { studiedToday } from "../time";
import { I18n } from "../i18n";
@@ -30,6 +30,10 @@ export function gatherData(data: pb.BackendProto.GraphsOut, i18n: I18n): TodayDa
continue;
}
+ if (review.reviewKind == ReviewKind.MANUAL) {
+ continue;
+ }
+
// total
answerCount += 1;
answerMillis += review.takenMillis;