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