diff --git a/pylib/anki/decks.py b/pylib/anki/decks.py index 4e980a5d7..755bc3ed6 100644 --- a/pylib/anki/decks.py +++ b/pylib/anki/decks.py @@ -5,7 +5,6 @@ from __future__ import annotations import copy import json -import operator import unicodedata from typing import Any, Dict, List, Optional, Set, Tuple, Union @@ -127,7 +126,7 @@ class DeckManager: # child of an existing deck then it needs to be renamed deck = self.get(did) if "::" in deck["name"]: - base = deck["name"].split("::")[-1] + base = self.basename(deck["name"]) suffix = "" while True: # find an unused name @@ -261,15 +260,15 @@ class DeckManager: ontoDeckName = self.get(ontoDeckDid)["name"] if ontoDeckDid is None or ontoDeckDid == "": - if len(self._path(draggedDeckName)) > 1: - self.rename(draggedDeck, self._basename(draggedDeckName)) + if len(self.path(draggedDeckName)) > 1: + self.rename(draggedDeck, self.basename(draggedDeckName)) elif self._canDragAndDrop(draggedDeckName, ontoDeckName): draggedDeck = self.get(draggedDeckDid) draggedDeckName = draggedDeck["name"] ontoDeckName = self.get(ontoDeckDid)["name"] assert ontoDeckName.strip() self.rename( - draggedDeck, ontoDeckName + "::" + self._basename(draggedDeckName) + draggedDeck, ontoDeckName + "::" + self.basename(draggedDeckName) ) def _canDragAndDrop(self, draggedDeckName: str, ontoDeckName: str) -> bool: @@ -283,24 +282,44 @@ class DeckManager: return True def _isParent(self, parentDeckName: str, childDeckName: str) -> Any: - return self._path(childDeckName) == self._path(parentDeckName) + [ - self._basename(childDeckName) + return self.path(childDeckName) == self.path(parentDeckName) + [ + self.basename(childDeckName) ] def _isAncestor(self, ancestorDeckName: str, descendantDeckName: str) -> Any: - ancestorPath = self._path(ancestorDeckName) - return ancestorPath == self._path(descendantDeckName)[0 : len(ancestorPath)] + ancestorPath = self.path(ancestorDeckName) + return ancestorPath == self.path(descendantDeckName)[0 : len(ancestorPath)] - def _path(self, name: str) -> Any: + @staticmethod + def path(name: str) -> Any: return name.split("::") - def _basename(self, name: str) -> Any: - return self._path(name)[-1] + _path = path + + @classmethod + def basename(cls, name: str) -> Any: + return cls.path(name)[-1] + + _basename = basename + + @classmethod + def immediate_parent_path(cls, name: str) -> Any: + return cls._path(name)[:-1] + + @classmethod + def immediate_parent(cls, name: str) -> Any: + pp = cls.immediate_parent_path(name) + if pp: + return "::".join(pp) + + @classmethod + def key(cls, deck: Dict[str, Any]) -> List[str]: + return cls.path(deck["name"]) def _ensureParents(self, name: str) -> Any: "Ensure parents exist, and return name with case matching parents." s = "" - path = self._path(name) + path = self.path(name) if len(path) < 2: return name for p in path[:-1]: @@ -454,7 +473,7 @@ class DeckManager: def _checkDeckTree(self) -> None: decks = self.col.decks.all() - decks.sort(key=operator.itemgetter("name")) + decks.sort(key=self.key) names: Set[str] = set() for deck in decks: @@ -465,14 +484,14 @@ class DeckManager: self.save(deck) # ensure no sections are blank - if not all(deck["name"].split("::")): + if not all(self.path(deck["name"])): self.col.log("fix deck with missing sections", deck["name"]) deck["name"] = "recovered%d" % intTime(1000) self.save(deck) # immediate parent must exist if "::" in deck["name"]: - immediateParent = "::".join(deck["name"].split("::")[:-1]) + immediateParent = self.immediate_parent(deck["name"]) if immediateParent not in names: self.col.log("fix deck with missing parent", deck["name"]) self._ensureParents(deck["name"]) @@ -570,14 +589,13 @@ class DeckManager: childMap = {} # go through all decks, sorted by name - for deck in sorted(self.all(), key=operator.itemgetter("name")): + for deck in sorted(self.all(), key=self.key): node: Dict[int, Any] = {} childMap[deck["id"]] = node # add note to immediate parent - parts = deck["name"].split("::") - if len(parts) > 1: - immediateParent = "::".join(parts[:-1]) + immediateParent = self.immediate_parent(deck["name"]) + if immediateParent is not None: pid = nameMap[immediateParent]["id"] childMap[pid][deck["id"]] = node @@ -587,7 +605,7 @@ class DeckManager: "All parents of did." # get parent and grandparent names parents: List[str] = [] - for part in self.get(did)["name"].split("::")[:-1]: + for part in self.immediate_parent_path(self.get(did)["name"]): if not parents: parents.append(part) else: @@ -605,7 +623,7 @@ class DeckManager: "All existing parents of name" if "::" not in name: return [] - names = name.split("::")[:-1] + names = self.immediate_parent_path(name) head = [] parents = [] diff --git a/pylib/anki/importing/anki2.py b/pylib/anki/importing/anki2.py index a43e0b13f..296b3b2c9 100644 --- a/pylib/anki/importing/anki2.py +++ b/pylib/anki/importing/anki2.py @@ -7,6 +7,7 @@ from typing import Any, Dict, List, Optional, Tuple from anki.collection import _Collection from anki.consts import * +from anki.decks import DeckManager from anki.importing.base import Importer from anki.lang import _ from anki.storage import Collection @@ -257,13 +258,13 @@ class Anki2Importer(Importer): name = g["name"] # if there's a prefix, replace the top level deck if self.deckPrefix: - tmpname = "::".join(name.split("::")[1:]) + tmpname = "::".join(DeckManager.path(name)[1:]) name = self.deckPrefix if tmpname: name += "::" + tmpname # manually create any parents so we can pull in descriptions head = "" - for parent in name.split("::")[:-1]: + for parent in DeckManager.immediate_parent_path(name): if head: head += "::" head += parent diff --git a/pylib/anki/sched.py b/pylib/anki/sched.py index 1e187e38d..997227b38 100644 --- a/pylib/anki/sched.py +++ b/pylib/anki/sched.py @@ -14,6 +14,7 @@ import anki from anki import hooks from anki.cards import Card from anki.consts import * +from anki.decks import DeckManager from anki.schedv2 import Scheduler as V2 from anki.utils import ids2str, intTime @@ -152,18 +153,11 @@ class Scheduler(V2): lims: Dict[str, List[int]] = {} data = [] - def parent(name): - parts = name.split("::") - if len(parts) < 2: - return None - parts = parts[:-1] - return "::".join(parts) - for deck in decks: - p = parent(deck["name"]) + p = DeckManager.immediate_parent(deck["name"]) # new nlim = self._deckNewLimitSingle(deck) - if p: + if p is not None: nlim = min(nlim, lims[p][0]) new = self._newForDeck(deck["id"], nlim) # learning diff --git a/pylib/anki/schedv2.py b/pylib/anki/schedv2.py index 75ab8b5d0..b4ba43c56 100644 --- a/pylib/anki/schedv2.py +++ b/pylib/anki/schedv2.py @@ -16,6 +16,7 @@ import anki # pylint: disable=unused-import from anki import hooks from anki.cards import Card from anki.consts import * +from anki.decks import DeckManager from anki.lang import _ from anki.rsbackend import FormatTimeSpanContext, SchedTimingToday from anki.utils import ids2str, intTime @@ -239,19 +240,12 @@ order by due""" lims: Dict[str, List[int]] = {} data = [] - def parent(name): - parts = name.split("::") - if len(parts) < 2: - return None - parts = parts[:-1] - return "::".join(parts) - childMap = self.col.decks.childMap() for deck in decks: - p = parent(deck["name"]) + p = DeckManager.immediate_parent(deck["name"]) # new nlim = self._deckNewLimitSingle(deck) - if p: + if p is not None: nlim = min(nlim, lims[p][0]) new = self._newForDeck(deck["id"], nlim) # learning @@ -279,7 +273,7 @@ order by due""" def _groupChildren(self, grps: List[List[Any]]) -> Any: # first, split the group names into components for g in grps: - g[0] = g[0].split("::") + g[0] = DeckManager.path(g[0]) # and sort based on those components grps.sort(key=itemgetter(0)) # then run main function diff --git a/pylib/anki/template.py b/pylib/anki/template.py index 97fde6699..5b14d33f0 100644 --- a/pylib/anki/template.py +++ b/pylib/anki/template.py @@ -34,6 +34,7 @@ from typing import Any, Dict, List, Optional, Tuple import anki from anki import hooks from anki.cards import Card +from anki.decks import DeckManager from anki.models import NoteType from anki.notes import Note from anki.rsbackend import TemplateReplacementList @@ -153,7 +154,7 @@ def fields_for_rendering( fields["Tags"] = note.stringTags().strip() fields["Type"] = card.note_type()["name"] fields["Deck"] = col.decks.name(card.odid or card.did) - fields["Subdeck"] = fields["Deck"].split("::")[-1] + fields["Subdeck"] = DeckManager.basename(fields["Deck"]) fields["Card"] = card.template()["name"] flag = card.userFlag() fields["CardFlag"] = flag and f"flag{flag}" or "" diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index d68c2078a..b107e2a08 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -20,6 +20,7 @@ from anki import hooks from anki.cards import Card from anki.collection import _Collection from anki.consts import * +from anki.decks import DeckManager from anki.lang import _, ngettext from anki.models import NoteType from anki.notes import Note @@ -1302,7 +1303,7 @@ QTableView {{ gridline-color: {grid} }} def addDecks(parent, decks): for head, did, rev, lrn, new, children in decks: name = self.mw.col.decks.get(did)["name"] - shortname = name.split("::")[-1] + shortname = DeckManager.basename(name) if children: subm = parent.addMenu(shortname) subm.addItem(_("Filter"), self._filterFunc("deck", name)) diff --git a/qt/aqt/studydeck.py b/qt/aqt/studydeck.py index 1a024b7c4..00538a880 100644 --- a/qt/aqt/studydeck.py +++ b/qt/aqt/studydeck.py @@ -3,6 +3,7 @@ # 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 * @@ -51,7 +52,10 @@ class StudyDeck(QDialog): if title: self.setWindowTitle(title) if not names: - names = sorted(self.mw.col.decks.allNames(dyn=dyn, force_default=False)) + names = sorted( + self.mw.col.decks.allNames(dyn=dyn, force_default=False), + key=DeckManager._path, + ) self.nameFunc = None self.origNames = names else: