diff --git a/anki/decks.py b/anki/decks.py index d692cfd4d..d07cbd2c9 100644 --- a/anki/decks.py +++ b/anki/decks.py @@ -135,9 +135,9 @@ class DeckManager: type = defaultDeck name = name.replace('"', '') name = unicodedata.normalize("NFC", name) - for id, g in list(self.decks.items()): - if unicodedata.normalize("NFC", g['name'].lower()) == name.lower(): - return int(id) + deck = self.byName(name) + if deck: + return int(deck["id"]) if not create: return None g = copy.deepcopy(type) @@ -242,9 +242,9 @@ class DeckManager: return self.decks['1'] def byName(self, name): - "Get deck with NAME." + """Get deck with NAME, ignoring cases.""" for m in list(self.decks.values()): - if m['name'] == name: + if self.equalName(m['name'], name): return m def update(self, g): @@ -257,7 +257,7 @@ class DeckManager: def rename(self, g, newName): "Rename deck prefix to NAME if not exists. Updates children." # make sure target node doesn't already exist - if newName in self.allNames(): + if self.byName(newName): raise DeckRenameError(_("That deck already exists.")) # make sure we're not nesting under a filtered deck for p in self.parentsByName(newName): @@ -454,7 +454,7 @@ class DeckManager: for deck in decks: # two decks with the same name? - if deck['name'] in names: + if self.normalizeName(deck['name']) in names: self.col.log("fix duplicate deck name", deck['name']) deck['name'] += "%d" % intTime(1000) self.save(deck) @@ -468,12 +468,12 @@ class DeckManager: # immediate parent must exist if "::" in deck['name']: immediateParent = "::".join(deck['name'].split("::")[:-1]) - if immediateParent not in names: + if self.normalizeName(immediateParent) not in names: self.col.log("fix deck with missing parent", deck['name']) self._ensureParents(deck['name']) - names.add(immediateParent) + names.add(self.normalizeName(immediateParent)) - names.add(deck['name']) + names.add(self.normalizeName(deck['name'])) def checkIntegrity(self): self._recoverOrphans() @@ -600,3 +600,11 @@ class DeckManager: def isDyn(self, did): return self.get(did)['dyn'] + + @staticmethod + def normalizeName(name): + return unicodedata.normalize("NFC", name.lower()) + + @staticmethod + def equalName(name1, name2): + return DeckManager.normalizeName(name1) == DeckManager.normalizeName(name2) diff --git a/tests/test_decks.py b/tests/test_decks.py index f127688f4..f88f335d3 100644 --- a/tests/test_decks.py +++ b/tests/test_decks.py @@ -1,5 +1,6 @@ # coding: utf-8 +from anki.errors import DeckRenameError from tests.shared import assertException, getEmptyCol def test_basic(): @@ -87,6 +88,20 @@ def test_rename(): d.decks.rename(d.decks.get(id), "yo") for n in "yo", "yo::two", "yo::two::three": assert n in d.decks.allNames() + # over filtered + filteredId = d.decks.newDyn("filtered") + filtered = d.decks.get(filteredId) + childId = d.decks.id("child") + child = d.decks.get(childId) + assertException(DeckRenameError, lambda: d.decks.rename(child, "filtered::child")) + assertException(DeckRenameError, lambda: d.decks.rename(child, "FILTERED::child")) + # changing case + parentId = d.decks.id("PARENT") + d.decks.id("PARENT::CHILD") + assertException(DeckRenameError, lambda: d.decks.rename(child, "PARENT::CHILD")) + assertException(DeckRenameError, lambda: d.decks.rename(child, "PARENT::child")) + + def test_renameForDragAndDrop(): d = getEmptyCol() @@ -130,6 +145,27 @@ def test_renameForDragAndDrop(): d.decks.renameForDragAndDrop(chinese_did, None) assert deckNames() == [ 'Chinese', 'Chinese::HSK', 'Languages' ] + # can't drack a deck where sibling have same name + new_hsk_did = d.decks.id("HSK") + assertException(DeckRenameError, lambda: d.decks.renameForDragAndDrop(new_hsk_did, chinese_did)) + d.decks.rem(new_hsk_did) + + # can't drack a deck where sibling have same name different case + new_hsk_did = d.decks.id("hsk") + assertException(DeckRenameError, lambda: d.decks.renameForDragAndDrop(new_hsk_did, chinese_did)) + d.decks.rem(new_hsk_did) + # '' is a convenient alias for the top level DID d.decks.renameForDragAndDrop(hsk_did, '') assert deckNames() == [ 'Chinese', 'HSK', 'Languages' ] + +def test_check(): + d = getEmptyCol() + + 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()