From 54fe8bb96fb89edf1ef1fa4afef550a9bc9909b6 Mon Sep 17 00:00:00 2001 From: Aaron Harsh Date: Sat, 7 Jan 2012 19:15:34 -0800 Subject: [PATCH] libanki support for drag-and-drop changes of deck hierarchy in deckbrowser --- anki/decks.py | 37 +++++++++++++++++++++++++++++++++--- anki/errors.py | 6 ++++++ tests/test_decks.py | 46 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 3 deletions(-) diff --git a/anki/decks.py b/anki/decks.py index f4948c571..e03ad5fd4 100644 --- a/anki/decks.py +++ b/anki/decks.py @@ -6,6 +6,7 @@ import simplejson, copy from anki.utils import intTime, ids2str from anki.consts import * from anki.lang import _ +from anki.errors import DeckRenameError # fixmes: # - make sure users can't set grad interval < 1 @@ -161,7 +162,7 @@ class DeckManager(object): "Rename deck prefix to NAME if not exists. Updates children." # make sure target node doesn't already exist if newName in self.allNames(): - raise Exception("Deck exists") + raise DeckRenameError(_("That deck already exists.")) # rename children for grp in self.all(): if grp['name'].startswith(g['name'] + "::"): @@ -174,10 +175,40 @@ class DeckManager(object): # finally, ensure we have parents self._ensureParents(newName) + def renameForDragAndDrop(self, draggedDeckDid, ontoDeckDid): + draggedDeck = self.get(draggedDeckDid) + draggedDeckName = draggedDeck['name'] + ontoDeckName = self.get(ontoDeckDid)['name'] + + if ontoDeckDid == None or ontoDeckDid == '': + 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'] + self.rename(draggedDeck, ontoDeckName + "::" + self._basename(draggedDeckName)) + + def _canDragAndDrop(self, draggedDeckName, ontoDeckName): + return draggedDeckName <> ontoDeckName \ + and not self._isParent(ontoDeckName, draggedDeckName) \ + and not self._isAncestor(draggedDeckName, ontoDeckName) + + def _isParent(self, parentDeckName, childDeckName): + return self._path(childDeckName) == self._path(parentDeckName) + [ self._basename(childDeckName) ] + + def _isAncestor(self, ancestorDeckName, descendantDeckName): + ancestorPath = self._path(ancestorDeckName) + return ancestorPath == self._path(descendantDeckName)[0:len(ancestorPath)] + + def _path(self, name): + return name.split("::") + def _basename(self, name): + return self._path(name)[-1] + def _ensureParents(self, name): - path = name.split("::") s = "" - for p in path[:-1]: + for p in self._path(name)[:-1]: if not s: s += p else: diff --git a/anki/errors.py b/anki/errors.py index 9f25339b6..4a683a8b5 100644 --- a/anki/errors.py +++ b/anki/errors.py @@ -11,3 +11,9 @@ class AnkiError(Exception): if self.data: m += ": %s" % repr(self.data) return m + +class DeckRenameError(Exception): + def __init__(self, description): + self.description = description + def __str__(self): + return "Couldn't rename deck: " + description diff --git a/tests/test_decks.py b/tests/test_decks.py index 486d9fa16..fcabf57cb 100644 --- a/tests/test_decks.py +++ b/tests/test_decks.py @@ -79,3 +79,49 @@ 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() + +def test_renameForDragAndDrop(): + d = getEmptyDeck() + + def deckNames(): + return [ name for name in sorted(d.decks.allNames()) if name <> u'Default' ] + + languages_did = d.decks.id('Languages') + chinese_did = d.decks.id('Chinese') + hsk_did = d.decks.id('Chinese::HSK') + + # Renaming also renames children + d.decks.renameForDragAndDrop(chinese_did, languages_did) + assert deckNames() == [ 'Languages', 'Languages::Chinese', 'Languages::Chinese::HSK' ] + + # Dragging a deck onto itself is a no-op + d.decks.renameForDragAndDrop(languages_did, languages_did) + assert deckNames() == [ 'Languages', 'Languages::Chinese', 'Languages::Chinese::HSK' ] + + # Dragging a deck onto its parent is a no-op + d.decks.renameForDragAndDrop(hsk_did, chinese_did) + assert deckNames() == [ 'Languages', 'Languages::Chinese', 'Languages::Chinese::HSK' ] + + # Dragging a deck onto a descendant is a no-op + d.decks.renameForDragAndDrop(languages_did, hsk_did) + assert deckNames() == [ 'Languages', 'Languages::Chinese', 'Languages::Chinese::HSK' ] + + # Can drag a grandchild onto its grandparent. It becomes a child + d.decks.renameForDragAndDrop(hsk_did, languages_did) + assert deckNames() == [ 'Languages', 'Languages::Chinese', 'Languages::HSK' ] + + # Can drag a deck onto its sibling + d.decks.renameForDragAndDrop(hsk_did, chinese_did) + assert deckNames() == [ 'Languages', 'Languages::Chinese', 'Languages::Chinese::HSK' ] + + # Can drag a deck back to the top level + d.decks.renameForDragAndDrop(chinese_did, None) + assert deckNames() == [ 'Chinese', 'Chinese::HSK', 'Languages' ] + + # Dragging a top level deck to the top level is a no-op + d.decks.renameForDragAndDrop(chinese_did, None) + assert deckNames() == [ 'Chinese', 'Chinese::HSK', 'Languages' ] + + # '' is a convenient alias for the top level DID + d.decks.renameForDragAndDrop(hsk_did, '') + assert deckNames() == [ 'Chinese', 'HSK', 'Languages' ]