1#!/usr/bin/env python3 2 3#****************************************************************************** 4# treestructure.py, provides a class to store the tree's data 5# 6# TreeLine, an information storage program 7# Copyright (C) 2018, Douglas W. Bell 8# 9# This is free software; you can redistribute it and/or modify it under the 10# terms of the GNU General Public License, either Version 2 or any later 11# version. This program is distributed in the hope that it will be useful, 12# but WITHOUT ANY WARRANTY. See the included LICENSE file for details. 13#****************************************************************************** 14 15import operator 16import copy 17import json 18import uuid 19import treenode 20import treeformats 21import undo 22try: 23 from __main__ import __version__ 24except ImportError: 25 __version__ = '' 26 27defaultRootTitle = _('Main') 28 29 30class TreeStructure(treenode.TreeNode): 31 """Class to store all tree data. 32 33 Inherits TreeNode to get childList (holds top nodes) and other methods. 34 """ 35 def __init__(self, fileData=None, topNodes=None, addDefaults=False, 36 addSpots=True): 37 """Create and store a tree structure from file data. 38 39 If no file data is given, create an empty or a default new structure. 40 Arguments: 41 fileData -- a dict in JSON file format of a structure 42 topNodes -- existing top-level nodes to add to a structure 43 addDefaults -- if True, adds default new structure 44 addSpots -- if True, adds parent spot references 45 """ 46 super().__init__(None) # init TreeNode, with no formatRef 47 self.nodeDict = {} 48 self.undoList = None 49 self.redoList = None 50 self.configDialogFormats = None 51 self.mathZeroBlanks = True 52 self.childRefErrorNodes = [] 53 if fileData: 54 self.treeFormats = treeformats.TreeFormats(fileData['formats']) 55 self.treeFormats.loadGlobalSavedConditions(fileData['properties']) 56 for nodeInfo in fileData['nodes']: 57 formatRef = self.treeFormats[nodeInfo['format']] 58 node = treenode.TreeNode(formatRef, nodeInfo) 59 self.nodeDict[node.uId] = node 60 for node in self.nodeDict.values(): 61 node.assignRefs(self.nodeDict) 62 if node.tmpChildRefs: 63 self.childRefErrorNodes.append(node) 64 node.tmpChildRefs = [] 65 for uId in fileData['properties']['topnodes']: 66 node = self.nodeDict[uId] 67 self.childList.append(node) 68 if 'zeroblanks' in fileData['properties']: 69 self.mathZeroBlanks = fileData['properties']['zeroblanks'] 70 if addSpots: 71 self.generateSpots(None) 72 elif topNodes: 73 self.childList = topNodes 74 self.treeFormats = treeformats.TreeFormats() 75 for topNode in topNodes: 76 for node in topNode.descendantGen(): 77 self.nodeDict[node.uId] = node 78 self.treeFormats.addTypeIfMissing(node.formatRef) 79 if addSpots: 80 self.generateSpots(None) 81 elif addDefaults: 82 self.treeFormats = treeformats.TreeFormats(setDefault=True) 83 node = treenode.TreeNode(self.treeFormats[treeformats. 84 defaultTypeName]) 85 node.setTitle(defaultRootTitle) 86 self.nodeDict[node.uId] = node 87 self.childList.append(node) 88 if addSpots: 89 self.generateSpots(None) 90 else: 91 self.treeFormats = treeformats.TreeFormats() 92 self.fileInfoNode = treenode.TreeNode(self.treeFormats.fileInfoFormat) 93 94 def fileData(self): 95 """Return a fileData dict in JSON file format. 96 """ 97 formats = self.treeFormats.storeFormats() 98 nodeList = sorted([node.fileData() for node in self.nodeDict.values()], 99 key=operator.itemgetter('uid')) 100 topNodeIds = [node.uId for node in self.childList] 101 properties = {'tlversion': __version__, 'topnodes': topNodeIds} 102 self.treeFormats.storeGlobalSavedConditions(properties) 103 if not self.mathZeroBlanks: 104 properties['zeroblanks'] = False 105 fileData = {'formats': formats, 'nodes': nodeList, 106 'properties': properties} 107 return fileData 108 109 def purgeOldFieldData(self): 110 """Remove data from obsolete fields from all nodes. 111 """ 112 fieldSets = self.treeFormats.fieldNameDict() 113 for node in self.nodeDict.values(): 114 oldKeys = set(node.data.keys()) - fieldSets[node.formatRef.name] 115 for key in oldKeys: 116 del node.data[key] 117 118 def addNodeDictRef(self, node): 119 """Add the given node to the node dictionary. 120 121 Arguments: 122 node -- the node to add 123 """ 124 self.nodeDict[node.uId] = node 125 126 def removeNodeDictRef(self, node): 127 """Remove the given node from the node dictionary. 128 129 Arguments: 130 node -- the node to remove 131 """ 132 try: 133 del self.nodeDict[node.uId] 134 except KeyError: 135 pass 136 137 def rebuildNodeDict(self): 138 """Remove and re-create the entire node dictionary. 139 """ 140 self.nodeDict = {} 141 for node in self.descendantGen(): 142 self.nodeDict[node.uId] = node 143 144 def replaceAllSpots(self, removeUnusedNodes=True): 145 """Remove and regenerate all spot refs for the tree. 146 147 Arguments: 148 removeUnusedNodes -- if True, delete refs to nodes without spots 149 """ 150 self.spotRefs = set() 151 for node in self.nodeDict.values(): 152 node.spotRefs = set() 153 self.generateSpots(None) 154 if removeUnusedNodes: 155 self.nodeDict = {uId:node for (uId, node) in self.nodeDict.items() 156 if node.spotRefs} 157 158 def deleteNodeSpot(self, spot): 159 """Remove the given spot, removing the entire node if no spots remain. 160 161 Arguments: 162 spot -- the spot to remove 163 """ 164 spot.parentSpot.nodeRef.childList.remove(spot.nodeRef) 165 for node in spot.nodeRef.descendantGen(): 166 if len(node.spotRefs) <= 1: 167 self.removeNodeDictRef(node) 168 node.spotRefs = set() 169 else: 170 node.removeInvalidSpotRefs(False) 171 172 def structSpot(self): 173 """Return the top spot (not tied to a node). 174 """ 175 (topSpot, ) = self.spotRefs 176 return topSpot 177 178 def rootSpots(self): 179 """Return a list of spots from root nodes. 180 """ 181 (topSpot, ) = self.spotRefs 182 return topSpot.childSpots() 183 184 def spotById(self, spotId): 185 """Return a spot based on a spot ID string. 186 187 Raises KeyError on invalid node ID, an IndexError on invalid spot num. 188 Arguments: 189 spotId -- a spot ID string, in the form "nodeID:spotInstance" 190 """ 191 nodeId, spotNum = spotId.split(':', 1) 192 return self.nodeDict[nodeId].spotByNumber(int(spotNum)) 193 194 def descendantGen(self): 195 """Return a generator to step through all nodes in tree order. 196 197 Override from TreeNode to exclude self. 198 """ 199 for child in self.childList: 200 for node in child.descendantGen(): 201 yield node 202 203 def getConfigDialogFormats(self, forceReset=False): 204 """Return duplicate formats for use in the config dialog. 205 206 Arguments: 207 forceReset -- if True, sets duplicate formats back to original 208 """ 209 if not self.configDialogFormats or forceReset: 210 self.configDialogFormats = copy.deepcopy(self.treeFormats) 211 return self.configDialogFormats 212 213 def applyConfigDialogFormats(self, addUndo=True): 214 """Replace the formats with the duplicates and signal for view update. 215 216 Also updates all nodes for changed type and field names. 217 """ 218 if addUndo: 219 undo.FormatUndo(self.undoList, self.treeFormats, 220 self.configDialogFormats) 221 self.treeFormats.copySettings(self.configDialogFormats) 222 self.treeFormats.updateDerivedRefs() 223 self.treeFormats.updateMathFieldRefs() 224 if self.configDialogFormats.fieldRenameDict: 225 for node in self.nodeDict.values(): 226 fieldRenameDict = (self.configDialogFormats.fieldRenameDict. 227 get(node.formatRef.name, {})) 228 tmpDataDict = {} 229 for oldName, newName in fieldRenameDict.items(): 230 if oldName in node.data: 231 tmpDataDict[newName] = node.data[oldName] 232 del node.data[oldName] 233 node.data.update(tmpDataDict) 234 self.configDialogFormats.fieldRenameDict = {} 235 if self.treeFormats.emptiedMathDict: 236 for node in self.nodeDict.values(): 237 for fieldName in self.treeFormats.emptiedMathDict.get(node. 238 formatRef. 239 name, 240 set()): 241 node.data.pop(fieldName, None) 242 self.formats.emptiedMathDict = {} 243 244 def usesType(self, typeName): 245 """Return true if any nodes use the give node format type. 246 247 Arguments: 248 typeName -- the format name to search for 249 """ 250 for node in self.nodeDict.values(): 251 if node.formatRef.name == typeName: 252 return True 253 return False 254 255 def replaceDuplicateIds(self, duplicateDict): 256 """Generate new unique IDs for any nodes found in newNodeDict. 257 258 Arguments: 259 newNodeDict -- a dict to search for duplicates 260 """ 261 for node in list(self.nodeDict.values()): 262 if node.uId in duplicateDict: 263 del self.nodeDict[node.uId] 264 node.uId = uuid.uuid1().hex 265 self.nodeDict[node.uId] = node 266 267 def addNodesFromStruct(self, treeStruct, parent, position=-1): 268 """Add nodes from the given structure under the given parent. 269 270 Arguments: 271 treeStruct -- the structure to insert 272 parent -- the parent of the new nodes 273 position -- the location to insert (-1 is appended) 274 """ 275 for nodeFormat in treeStruct.treeFormats.values(): 276 self.treeFormats.addTypeIfMissing(nodeFormat) 277 for node in treeStruct.nodeDict.values(): 278 self.nodeDict[node.uId] = node 279 node.formatRef = self.treeFormats[node.formatRef.name] 280 for node in treeStruct.childList: 281 if position >= 0: 282 parent.childList.insert(position, node) 283 position += 1 284 else: 285 parent.childList.append(node) 286 node.addSpotRef(parent) 287 288 def debugCheck(self): 289 """Run debugging checks on structure nodeDict, nodes and spots. 290 291 Reports results to std output. Not to be run in production releases. 292 """ 293 print('\nChecking nodes in nodeDict:') 294 nodeIds = set() 295 errorCount = 0 296 for node in self.descendantGen(): 297 nodeIds.add(node.uId) 298 if node.uId not in self.nodeDict: 299 print(' Node not in nodeDict, ID: {}, Title: {}'. 300 format(node.uId, node.title())) 301 errorCount += 1 302 for uId in set(self.nodeDict.keys()) - nodeIds: 303 node = self.nodeDict[uId] 304 print(' Node not in structure, ID: {}, Title: {}'. 305 format(node.uId, node.title())) 306 errorCount += 1 307 print(' {} errors found in nodeDict'.format(errorCount)) 308 309 print('\nChecking spots:') 310 errorCount = 0 311 for topNode in self.childList: 312 for node in topNode.descendantGen(): 313 for spot in node.spotRefs: 314 if node not in spot.parentSpot.nodeRef.childList: 315 print(' Invalid spot in node, ID: {}, Title: {}'. 316 format(node.uId, node.title())) 317 errorCount += 1 318 for child in node.childList: 319 if len(child.spotRefs) < len(node.spotRefs): 320 print(' Missing spot in node, ID: {}, Title: {}'. 321 format(child.uId, child.title())) 322 errorCount += 1 323 print(' {} errors found in spots'.format(errorCount)) 324 325 326#### Utility Functions #### 327 328def structFromMimeData(mimeData): 329 """Return a tree structure based on mime data. 330 331 Arguments: 332 mimeData -- data to be used 333 """ 334 try: 335 data = json.loads(str(mimeData.data('application/json'), 'utf-8')) 336 return TreeStructure(data, addSpots=False) 337 except (ValueError, KeyError, TypeError): 338 return None 339