From 3f4356467ef9f9ec580091eaaff9d4430e07b736 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 17 Apr 2022 18:32:22 +0200 Subject: [PATCH] Relax moving restrictions in project tree, and improve test coverage of NWItem class --- novelwriter/core/item.py | 9 ++- novelwriter/gui/projtree.py | 18 ++--- tests/test_core/test_core_item.py | 115 +++++++++++++++++++++++++++++- 3 files changed, 130 insertions(+), 12 deletions(-) diff --git a/novelwriter/core/item.py b/novelwriter/core/item.py index 3a741f62..0178c512 100644 --- a/novelwriter/core/item.py +++ b/novelwriter/core/item.py @@ -259,7 +259,7 @@ class NWItem(): return ## - # Methods + # Lookup Methods ## def describeMe(self, hLevel=None): @@ -296,8 +296,7 @@ class NWItem(): return self._class in (nwItemClass.NOVEL, nwItemClass.ARCHIVE, nwItemClass.TRASH) def isInactive(self): - """Returns true if the item is in the inactive parts of the - project. + """Returns true if the item is in an inactive class. """ return self._class in (nwItemClass.NO_CLASS, nwItemClass.ARCHIVE, nwItemClass.TRASH) @@ -313,6 +312,10 @@ class NWItem(): stIcon = self.theProject.importItems.icon(self._import) return stName, stIcon + ## + # Special Setters + ## + def setImportStatus(self, value): """Update the importance or status value based on class. This is a wrapper setter for setStatus and setImport. diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index c75a0f58..baefcfea 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -37,7 +37,7 @@ from PyQt5.QtWidgets import ( from novelwriter.core import NWDoc from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert -from novelwriter.constants import trConst, nwLists, nwLabels +from novelwriter.constants import trConst, nwLabels logger = logging.getLogger(__name__) @@ -790,21 +790,23 @@ class GuiProjectTree(QTreeWidget): if pItem is not None: pIndex = pItem.indexOfChild(sItem) - wCount = int(sItem.data(self.C_COUNT, Qt.UserRole)) + # Determine if the drag and drop is allowed: + # - Files can be moved anywhere + # - Folders can only be moved within the same root folder + # - Root folders cannot be moved at all + # - Items cannot be dropped on top of a file (moved inside) + isFile = snItem.itemType == nwItemType.FILE isRoot = snItem.itemType == nwItemType.ROOT onFile = dnItem.itemType == nwItemType.FILE + inSame = snItem.itemRoot == dnItem.itemRoot - isSame = snItem.itemClass == dnItem.itemClass - isNone = snItem.itemClass == nwItemClass.NO_CLASS - isNote = snItem.itemLayout == nwItemLayout.NOTE - onFree = dnItem.itemClass in nwLists.FREE_CLASS and isFile - - allowDrop = isSame or isNone or isNote or onFree + allowDrop = inSame or isFile allowDrop &= not (self.dropIndicatorPosition() == QAbstractItemView.OnItem and onFile) if allowDrop and not isRoot: logger.debug("Drag'n'drop of item '%s' accepted", sHandle) + wCount = int(sItem.data(self.C_COUNT, Qt.UserRole)) self.propagateCount(sHandle, 0) QTreeWidget.dropEvent(self, theEvent) self._postItemMove(sHandle, wCount) diff --git a/tests/test_core/test_core_item.py b/tests/test_core/test_core_item.py index b86fe968..8f65bb05 100644 --- a/tests/test_core/test_core_item.py +++ b/tests/test_core/test_core_item.py @@ -220,6 +220,7 @@ def testCoreItem_Methods(mockGUI): # Status + Icon # ============= + theItem.setType("FILE") theItem.setStatus("Note") theItem.setImport("Minor") @@ -229,11 +230,19 @@ def testCoreItem_Methods(mockGUI): assert stT == "Note" assert isinstance(stI, QIcon) + theItem.setImportStatus("Draft") + stT, stI = theItem.getImportStatus() + assert stT == "Draft" + theItem.setClass("CHARACTER") stT, stI = theItem.getImportStatus() assert stT == "Minor" assert isinstance(stI, QIcon) + theItem.setImportStatus("Major") + stT, stI = theItem.getImportStatus() + assert stT == "Major" + # Representation # ============== @@ -275,6 +284,8 @@ def testCoreItem_TypeSetter(mockGUI): assert theItem.itemType == nwItemType.FILE theItem.setType("TRASH") assert theItem.itemType == nwItemType.TRASH + + # Alternative theItem.setType(nwItemType.ROOT) assert theItem.itemType == nwItemType.ROOT @@ -294,28 +305,74 @@ def testCoreItem_ClassSetter(mockGUI): assert theItem.itemClass == nwItemClass.NO_CLASS theItem.setClass("NONSENSE") assert theItem.itemClass == nwItemClass.NO_CLASS + theItem.setClass("NO_CLASS") assert theItem.itemClass == nwItemClass.NO_CLASS + assert theItem.isNovelLike() is False + assert theItem.documentAllowed() is False + assert theItem.isInactive() is True + theItem.setClass("NOVEL") assert theItem.itemClass == nwItemClass.NOVEL + assert theItem.isNovelLike() is True + assert theItem.documentAllowed() is True + assert theItem.isInactive() is False + theItem.setClass("PLOT") assert theItem.itemClass == nwItemClass.PLOT + assert theItem.isNovelLike() is False + assert theItem.documentAllowed() is False + assert theItem.isInactive() is False + theItem.setClass("CHARACTER") assert theItem.itemClass == nwItemClass.CHARACTER + assert theItem.isNovelLike() is False + assert theItem.documentAllowed() is False + assert theItem.isInactive() is False + theItem.setClass("WORLD") assert theItem.itemClass == nwItemClass.WORLD + assert theItem.isNovelLike() is False + assert theItem.documentAllowed() is False + assert theItem.isInactive() is False + theItem.setClass("TIMELINE") assert theItem.itemClass == nwItemClass.TIMELINE + assert theItem.isNovelLike() is False + assert theItem.documentAllowed() is False + assert theItem.isInactive() is False + theItem.setClass("OBJECT") assert theItem.itemClass == nwItemClass.OBJECT + assert theItem.isNovelLike() is False + assert theItem.documentAllowed() is False + assert theItem.isInactive() is False + theItem.setClass("ENTITY") assert theItem.itemClass == nwItemClass.ENTITY + assert theItem.isNovelLike() is False + assert theItem.documentAllowed() is False + assert theItem.isInactive() is False + theItem.setClass("CUSTOM") assert theItem.itemClass == nwItemClass.CUSTOM + assert theItem.isNovelLike() is False + assert theItem.documentAllowed() is False + assert theItem.isInactive() is False + theItem.setClass("ARCHIVE") assert theItem.itemClass == nwItemClass.ARCHIVE + assert theItem.isNovelLike() is True + assert theItem.documentAllowed() is True + assert theItem.isInactive() is True + theItem.setClass("TRASH") assert theItem.itemClass == nwItemClass.TRASH + assert theItem.isNovelLike() is False + assert theItem.documentAllowed() is True + assert theItem.isInactive() is True + + # Alternative theItem.setClass(nwItemClass.NOVEL) assert theItem.itemClass == nwItemClass.NOVEL @@ -344,13 +401,69 @@ def testCoreItem_LayoutSetter(mockGUI): theItem.setLayout("NOTE") assert theItem.itemLayout == nwItemLayout.NOTE - # Alternatives + # Alternative theItem.setLayout(nwItemLayout.NOTE) assert theItem.itemLayout == nwItemLayout.NOTE # END Test testCoreItem_LayoutSetter +@pytest.mark.core +def testCoreItem_ClassDefaults(mockGUI): + """Test the setter for the default values. + """ + theProject = NWProject(mockGUI) + theItem = NWItem(theProject) + + # Root items should not have their class updated + theItem.setParent(None) + theItem.setClass(nwItemClass.NO_CLASS) + assert theItem.itemClass == nwItemClass.NO_CLASS + + theItem.setClassDefaults(nwItemClass.NOVEL) + assert theItem.itemClass == nwItemClass.NO_CLASS + + # Non-root items should have their class updated + theItem.setParent("0123456789abc") + theItem.setClass(nwItemClass.NO_CLASS) + assert theItem.itemClass == nwItemClass.NO_CLASS + + theItem.setClassDefaults(nwItemClass.NOVEL) + assert theItem.itemClass == nwItemClass.NOVEL + + # Non-layout items should have their layout set based on class + theItem.setParent("0123456789abc") + theItem.setClass(nwItemClass.NO_CLASS) + theItem.setLayout(nwItemLayout.NO_LAYOUT) + assert theItem.itemLayout == nwItemLayout.NO_LAYOUT + + theItem.setClassDefaults(nwItemClass.NOVEL) + assert theItem.itemLayout == nwItemLayout.DOCUMENT + + theItem.setParent("0123456789abc") + theItem.setClass(nwItemClass.NO_CLASS) + theItem.setLayout(nwItemLayout.NO_LAYOUT) + assert theItem.itemLayout == nwItemLayout.NO_LAYOUT + + theItem.setClassDefaults(nwItemClass.PLOT) + assert theItem.itemLayout == nwItemLayout.NOTE + + # If documents are not allowed in that class, the layout should be changed + theItem.setParent("0123456789abc") + theItem.setClass(nwItemClass.NO_CLASS) + theItem.setLayout(nwItemLayout.DOCUMENT) + assert theItem.itemLayout == nwItemLayout.DOCUMENT + + theItem.setClassDefaults(nwItemClass.PLOT) + assert theItem.itemLayout == nwItemLayout.NOTE + + # In all cases, status and importance should no longer be None + assert theItem.itemStatus is not None + assert theItem.itemImport is not None + +# END Test testCoreItem_ClassDefaults + + @pytest.mark.core def testCoreItem_XMLPackUnpack(mockGUI, caplog, constData): """Test packing and unpacking XML objects for the NWItem class.