def doAction(self): treeView = self.parent() # Collect tags tags = set() from maestro.widgets.browser.model import TagLayer for layer in treeView.model().layers: if isinstance(layer, TagLayer): tags.update(layer.tagList) # Collect values valuesToHide = {tag: [] for tag in tags} for wrapper in treeView.selection.wrappers(): if not wrapper.isContainer(): continue # Collect all children ids and tag values wrapper.loadContents(recursive=True) ids = [] allValues = {tag: set() for tag in tags} for child in wrapper.getAllNodes(skipSelf=True): element = child.element ids.append(element.id) for tag in tags: if tag in element.tags: allValues[tag].update( db.tags.id(tag, value) for value in element.tags[tag]) # Check which tag values appear outside the children ids (thus possibly in wrapper itself) for tag, vids in allValues.items(): if len(vids) > 0: result = db.query(""" SELECT value_id FROM {p}tags WHERE tag_id=? AND element_id NOT IN ({elids}) AND value_id IN ({vids}) GROUP BY value_id """, tag.id, elids=db.csList(ids), vids=db.csList(vids)) # We will not hide the values returned from the query because they appear elsewhere vids.difference_update(result.getSingleColumn()) # these lists should be disjoint for different wrappers # (unless one is an ancestor of the other one) valuesToHide[tag].extend(vids) dialog = HideTagValuesDialog(treeView, valuesToHide) dialog.exec_()
def accept(self): from maestro.plugins.coverdesk.plugin import StackItem, CoverItem stack = StackItem(self.titleEdit.text()) if self.allButton.isChecked(): toplevel = list( db.query( """ SELECT id FROM {p}elements WHERE domain=? AND id NOT IN (SELECT element_id FROM {p}contents) ORDER BY id """, self.scene.domain.id).getSingleColumn()) elements = levels.real.collect(toplevel) stack.items = [CoverItem(self.scene, el) for el in elements] elif self.searchButton.isChecked(): criterion = self.searchBox.criterion if criterion is None: return # do not accept search.search(criterion, domain=self.scene.domain) elids = criterion.result toplevel = set(elids) toplevel.difference_update( db.query( "SELECT element_id FROM {p}contents WHERE container_id IN ({elids})", elids=db.csList(elids)).getSingleColumn()) toplevel = sorted(elids) elements = levels.real.collect(toplevel) stack.items = [CoverItem(self.scene, el) for el in elements] stack.setPos(self.scene.makePosition(self.position)) self.scene.addItem(stack) super().accept()
def _loaded(self, task): """Load covers after search for elements has finished. If no search was necessary, *task* is None. """ if task is not None: if not isinstance(task, search.SearchTask): # subclasses might submit over tasks return elids = task.criterion.result if len(elids): filterClause = " AND el.id IN ({})".format(db.csList(elids)) else: self.display().setCovers([], {}) return else: filterClause = " AND el.domain={}".format(self.domain.id) result = db.query(""" SELECT el.id, st.data FROM {p}elements AS el JOIN {p}stickers AS st ON el.id = st.element_id WHERE st.type = 'COVER' {filter} """, filter=filterClause) coverPaths = {id: path for id, path in result} ids = list(coverPaths.keys()) levels.real.collect(ids) if tags.isInDb("artist") and tags.isInDb("date"): sortValues = {} artistTag = tags.get("artist") dateTag = tags.get("date") for id in ids: el = levels.real[id] sortValues[id] = (el.tags[artistTag][0] if artistTag in el.tags else utils.PointAtInfinity(), el.tags[dateTag][0] if dateTag in el.tags else utils.PointAtInfinity()) ids.sort(key=sortValues.get) self.display().setCovers(ids, coverPaths)
def accept(self): from maestro.plugins.coverdesk.plugin import StackItem, CoverItem stack = StackItem(self.titleEdit.text()) if self.allButton.isChecked(): toplevel = list(db.query(""" SELECT id FROM {p}elements WHERE domain=? AND id NOT IN (SELECT element_id FROM {p}contents) ORDER BY id """, self.scene.domain.id).getSingleColumn()) elements = levels.real.collect(toplevel) stack.items = [CoverItem(self.scene, el) for el in elements] elif self.searchButton.isChecked(): criterion = self.searchBox.criterion if criterion is None: return # do not accept search.search(criterion, domain=self.scene.domain) elids = criterion.result toplevel = set(elids) toplevel.difference_update(db.query( "SELECT element_id FROM {p}contents WHERE container_id IN ({elids})", elids=db.csList(elids)).getSingleColumn()) toplevel = sorted(elids) elements = levels.real.collect(toplevel) stack.items = [CoverItem(self.scene, el) for el in elements] stack.setPos(self.scene.makePosition(self.position)) self.scene.addItem(stack) super().accept()
def _removeFromDb(self, elements): """Like removeFromDb but not undoable.""" for element in elements: assert element.isInDb() if element.isContainer(): del self.elements[element.id] for childId in element.contents: self[childId].parents.remove(element.id) _dbIds.difference_update(element.id for element in elements) # Rely on foreign keys to delete all tags, flags etc. from the database ids = itertools.chain.from_iterable(element.contents for element in elements if element.isContainer()) db.query("DELETE FROM {}elements WHERE id IN ({})".format( db.prefix, db.csList(element.id for element in elements))) removedFiles = [ element.url for element in elements if element.isFile() and element.url.scheme == "file" ] if len(removedFiles) > 0: self.emitFilesystemEvent(removed=removedFiles) self.checkGlobalSelection(elements) self.emit( levels.LevelChangeEvent(dbRemovedIds=[el.id for el in elements]))
def doAction(self): treeView = self.parent() # Collect tags tags = set() from maestro.widgets.browser.model import TagLayer for layer in treeView.model().layers: if isinstance(layer, TagLayer): tags.update(layer.tagList) # Collect values valuesToHide = {tag: [] for tag in tags} for wrapper in treeView.selection.wrappers(): if not wrapper.isContainer(): continue # Collect all children ids and tag values wrapper.loadContents(recursive=True) ids = [] allValues = {tag: set() for tag in tags} for child in wrapper.getAllNodes(skipSelf=True): element = child.element ids.append(element.id) for tag in tags: if tag in element.tags: allValues[tag].update(db.tags.id(tag, value) for value in element.tags[tag]) # Check which tag values appear outside the children ids (thus possibly in wrapper itself) for tag, vids in allValues.items(): if len(vids) > 0: result = db.query(""" SELECT value_id FROM {p}tags WHERE tag_id=? AND element_id NOT IN ({elids}) AND value_id IN ({vids}) GROUP BY value_id """, tag.id, elids=db.csList(ids), vids=db.csList(vids)) # We will not hide the values returned from the query because they appear elsewhere vids.difference_update(result.getSingleColumn()) # these lists should be disjoint for different wrappers # (unless one is an ancestor of the other one) valuesToHide[tag].extend(vids) dialog = HideTagValuesDialog(treeView, valuesToHide) dialog.exec_()
def _removeFromDb(self, elements): """Like removeFromDb but not undoable.""" for element in elements: assert element.isInDb() if element.isContainer(): del self.elements[element.id] for childId in element.contents: self[childId].parents.remove(element.id) _dbIds.difference_update(element.id for element in elements) # Rely on foreign keys to delete all tags, flags etc. from the database ids = itertools.chain.from_iterable(element.contents for element in elements if element.isContainer()) db.query("DELETE FROM {}elements WHERE id IN ({})" .format(db.prefix, db.csList(element.id for element in elements))) removedFiles = [element.url for element in elements if element.isFile() and element.url.scheme == "file"] if len(removedFiles) > 0: self.emitFilesystemEvent(removed=removedFiles) self.checkGlobalSelection(elements) self.emit(levels.LevelChangeEvent(dbRemovedIds=[el.id for el in elements]))
def loadFromDb(self, idList, level=None): """Load elements specified by *idList* from the database into *level* which defaults to the real level.""" if level is None: level = self if len(idList) == 0: # queries will fail otherwise return [] csIdList = db.csList(idList) # bare elements result = db.query(""" SELECT el.domain, el.id, el.file, el.type, f.url, f.length FROM {0}elements AS el LEFT JOIN {0}files AS f ON el.id = f.element_id WHERE el.id IN ({1}) """.format(db.prefix, csIdList)) for domainId, id, file, elementType, url, length in result: _dbIds.add(id) if file: level.elements[id] = elements.File(domains.domainById(domainId), level, id, url=URL(url), length=length, type=elements.ContainerType(elementType)) else: level.elements[id] = elements.Container(domains.domainById(domainId), level, id, type=elements.ContainerType(elementType)) # contents result = db.query(""" SELECT el.id, c.position, c.element_id FROM {0}elements AS el JOIN {0}contents AS c ON el.id = c.container_id WHERE el.id IN ({1}) ORDER BY position """.format(db.prefix, csIdList)) for id, pos, contentId in result: level.elements[id].contents.insert(pos, contentId) # parents result = db.query(""" SELECT el.id, c.container_id FROM {0}elements AS el JOIN {0}contents AS c ON el.id = c.element_id WHERE el.id IN ({1}) """.format(db.prefix, csIdList)) for id, contentId in result: level.elements[id].parents.append(contentId) # tags result = db.query(""" SELECT el.id, t.tag_id, t.value_id FROM {0}elements AS el JOIN {0}tags AS t ON el.id = t.element_id WHERE el.id IN ({1}) """.format(db.prefix, csIdList)) for id, tagId, valueId in result: tag = tags.get(tagId) level.elements[id].tags.add(tag, db.tags.value(tag, valueId)) # flags result = db.query(""" SELECT el.id, f.flag_id FROM {0}elements AS el JOIN {0}flags AS f ON el.id = f.element_id WHERE el.id IN ({1}) """.format(db.prefix, csIdList)) for id, flagId in result: level.elements[id].flags.append(flags.get(flagId)) # stickers result = db.query(""" SELECT element_id, type, data FROM {}stickers WHERE element_id IN ({}) ORDER BY element_id, type, sort """.format(db.prefix, csIdList)) # This is a bit complicated because the stickers should be stored in tuples, not lists # Changing the lists would break undo/redo #TODO: is this really necessary? current = None buffer = [] for (id, type, sticker) in result: if current is None: current = (id, type) elif current != (id, type): level.elements[current[0]].stickers[current[1]] = tuple(buffer) current = (id, type) buffer = [] element = level.elements[id] if element.stickers is None: element.stickers = {} buffer.append(sticker) if current is not None: level.elements[current[0]].stickers[current[1]] = tuple(buffer) try: return [self.elements[id] for id in idList] except KeyError: # probably some ids were not contained in the database raise levels.ElementGetError(self, [id for id in idList if id not in self])
def _buildContainerTree(domain, elids): """Create a wrapper tree including all elements from *elids* (or all elements with the given domain, if *elids* is None). The tree will organize wrappers according to the natural tree structure. """ if elids is None: toplevel = list( db.query( """ SELECT id FROM {p}elements WHERE domain=? AND id NOT IN (SELECT element_id FROM {p}contents) """, domain.id).getSingleColumn()) elif len(elids) > 0: toplevel = set(elids) toplevel.difference_update( db.query( "SELECT element_id FROM {}contents WHERE container_id IN ({})". format(db.prefix, db.csList(elids))).getSingleColumn()) else: return [] # Load all toplevel elements and all of their ancestors newIds = toplevel while len(newIds) > 0: levels.real.collect(newIds) nextIds = [] for id in newIds: nextIds.extend(levels.real[id].parents) newIds = nextIds # Collect all parents in cDict (mapping parent id -> list of children ids) # Parents contained as key in this dict, will only contain part of their element's contants in # the browser. The part is given by the corresponding value (a list) in this dict. # The dict must not contain direct search results as keys, as they should always show all their # contents. cDict = collections.defaultdict(list) def processNode(id): """Check whether the element with the given id has major parents that need to be added to the browser's tree. If such a parent is found, update cDict and toplevel and return True. """ result = False for pid in levels.real[id].parents: if pid in cDict: # We've already added this parent result = True cDict[pid].append(id) toplevel.discard(id) elif elids is None or pid in elids: # This parent belongs to the direct search result result = True toplevel.discard(id) elif processNode( pid ): # We must add this parent, because it has a major ancestor. result = True cDict[pid].append(id) toplevel.discard(id) elif levels.real[ pid].type.major: # This is a major parent. Add it to toplevel result = True cDict[pid].append(id) toplevel.discard(id) toplevel.add(pid) return result for id in list(toplevel): # copy! processNode(id) def createWrapper(id): """Create a wrapper to be inserted in the browser. If the wrapper should contain all of its element's contents, create a BrowserWrapper, that will load the contents """ element = levels.real[id] if id in cDict: # wrapper should contain only a part of its element's contents wrapper = Wrapper(element) wrapper.setContents([createWrapper(cid) for cid in cDict[id]]) return wrapper elif element.isFile() or len(element.contents) == 0: return Wrapper(element) else: return bnodes.BrowserWrapper( element ) # a wrapper that will load all its contents when needed contents = [createWrapper(id) for id in toplevel] def sortFunction(wrapper): """Intelligent sort: sort albums by their date, everything else by name.""" element = wrapper.element date = 0 if element.isContainer( ) and element.type == elements.ContainerType.Album: dateTag = tags.get("date") if dateTag.type == tags.TYPE_DATE and dateTag in element.tags: date = -element.tags[dateTag][0].toSql( ) # minus leads to descending sort return (date, element.getTitle(neverShowIds=True)) contents.sort(key=sortFunction) return contents
def build(self, layerIndex, domain, elids, matchingTags): # 1. Get toplevel nodes. if elids is None: toplevel = set( db.query( """ SELECT id FROM {p}elements WHERE domain=? AND id NOT IN (SELECT element_id FROM {p}contents) """, domain.id).getSingleColumn()) if len(toplevel) == 0: return [] elif len(elids) > 0: toplevel = set(elids) toplevel.difference_update( db.query( "SELECT element_id FROM {p}contents WHERE container_id IN ({elids})", elids=db.csList(elids)).getSingleColumn()) else: return [] # Shortcut: For very small result sets simply use a container tree if len(toplevel) <= 5: return _buildContainerTree(domain, elids) # 2. Check whether a VariousNode is necessary. # (that is, some toplevel nodes don't have a tag from self.tagList) # We do this so early because 'toplevel' will be enlarged in the next step. tagFilter = db.csIdList(self.tagList) idFilter = db.csList(toplevel) variousNodeElements = list( db.query(""" SELECT el.id FROM {p}elements AS el LEFT JOIN {p}tags AS t ON el.id = t.element_id AND t.tag_id IN ({tagFilter}) WHERE domain={domain} AND el.id IN ({idFilter}) AND t.value_id IS NULL LIMIT 1 """, tagFilter=tagFilter, idFilter=idFilter, domain=domain.id).getSingleColumn()) # 3. Add contents of permeable nodes to 'toplevel', as long as they are in the search result. # Tag values in these nodes should get a TagNode even if # they don't appear in an actual toplevel node. new = toplevel while len(new): new = set( db.query(""" SELECT c.element_id FROM {p}contents AS c JOIN {p}elements AS el ON c.container_id = el.id WHERE el.type IN ({collection},{container}) AND el.id IN ({parents})""", collection=elements.ContainerType.Collection.value, container=elements.ContainerType.Container.value, parents=db.csList(new)).getSingleColumn()) # Restrict to search result. If the node's value only appears in contents of a permeable # node in the search result and these contents are not in the result themselves, # we would create an empty TagNode. if elids is not None: new.intersection_update(elids) toplevel.update(new) # 4. Create a TagNode for each tag value that appears in 'toplevel' # Make sure to use as single TagNode for equal values in different tags nodes = collections.defaultdict( functools.partial(bnodes.TagNode, layerIndex)) idFilter = db.csList(toplevel) result = db.query(""" SELECT DISTINCT t.tag_id, v.id, v.value, v.hide, v.sort_value FROM {p}tags AS t JOIN {p}values_varchar AS v ON t.tag_id = v.tag_id AND t.value_id = v.id WHERE t.tag_id IN ({tagFilter}) AND t.element_id IN ({idFilter}) """, tagFilter=tagFilter, idFilter=idFilter) for tagId, valueId, value, hide, sortValue in result: matching = (tagId, valueId) in matchingTags nodes[value].addTagValue(tagId, valueId, value, hide, sortValue, matching) # 5. Optimize TagNodes (if there are only few of them) if len(nodes) <= 20: # Above we had to use values as keys, here ids are more useful nodes = { tagTuple: node for node in nodes.values() for tagTuple in node.tagIds } # The first task is to find all contents of each TagNode. Note that the last query (to find # TagNodes) only considered toplevel elements. for node in nodes.values(): node.elids = set() if elids is not None: idFilter = "t.element_id IN ({})".format(db.csList(elids)) else: idFilter = '1' result = db.query(""" SELECT DISTINCT tag_id, value_id, element_id FROM {p}tags AS t WHERE t.tag_id IN ({tagFilter}) AND {idFilter} """, tagFilter=tagFilter, idFilter=idFilter) withinMatchingTags = set() for tagId, valueId, elementId in result: if (tagId, valueId) in nodes: node = nodes[(tagId, valueId)] else: continue # this means that this value does not appear in a 'toplevel' node node.elids.add(elementId) if (tagId, valueId) in matchingTags: withinMatchingTags.add(elementId) def checkSuperNode(node, superNode): """Given two nodes where the second contains all contents of the first, return whether the first node may be deleted. As a sideeffect this method may merge *node* into *superNode*. """ # If *node* is completely contained in superNode, delete it. if len(node.elids) < len(superNode.elids): # However, matching nodes must not be deleted in favor of not matching ones. # and visible nodes must not be deleted in favor of a hidden superNode. return not ((node.matching and not superNode.matching) or (not node.hide and superNode.hide)) else: if node.hide == superNode.hide: superNode.merge(node) return True else: return node.hide for k in list(nodes.keys()): node = nodes[k] # Delete nodes whose contents are covered by nodes matching the search query. if not node.matching and node.elids <= withinMatchingTags: del nodes[k] continue # Try to delete (or merge) TagNodes whose contents are contained in (or equal to) another # TagNode for node2 in nodes.values(): if node2 is not node and node.elids <= node2.elids: # node2 is a superNode: if checkSuperNode(node, node2): del nodes[k] break # 6. Create final list of nodes visibleNodes = [node for node in nodes.values() if not node.hide] hiddenNodes = [node for node in nodes.values() if node.hide] visibleNodes.sort( key=lambda node: locale.strxfrm(node.sortValues[0][0])) hiddenNodes.sort( key=lambda node: locale.strxfrm(node.sortValues[0][0])) if len(variousNodeElements) > 0: node = bnodes.VariousNode(layerIndex, self.tagList) visibleNodes.append(node) if len(hiddenNodes) > 0: # If hidden nodes are present this layer needs two actual levels in the tree structure # Since this interferes with the algorithm to determine the layer of a node, we have to store # that layer index. See BrowserModel._getLayerIndex for node in hiddenNodes: node.layer = self visibleNodes.append(bnodes.HiddenValuesNode(hiddenNodes)) return visibleNodes
def _buildContainerTree(domain, elids): """Create a wrapper tree including all elements from *elids* (or all elements with the given domain, if *elids* is None). The tree will organize wrappers according to the natural tree structure. """ if elids is None: toplevel = list(db.query(""" SELECT id FROM {p}elements WHERE domain=? AND id NOT IN (SELECT element_id FROM {p}contents) """, domain.id).getSingleColumn()) elif len(elids) > 0: toplevel = set(elids) toplevel.difference_update(db.query( "SELECT element_id FROM {}contents WHERE container_id IN ({})" .format(db.prefix, db.csList(elids))).getSingleColumn()) else: return [] # Load all toplevel elements and all of their ancestors newIds = toplevel while len(newIds) > 0: levels.real.collect(newIds) nextIds = [] for id in newIds: nextIds.extend(levels.real[id].parents) newIds = nextIds # Collect all parents in cDict (mapping parent id -> list of children ids) # Parents contained as key in this dict, will only contain part of their element's contants in # the browser. The part is given by the corresponding value (a list) in this dict. # The dict must not contain direct search results as keys, as they should always show all their # contents. cDict = collections.defaultdict(list) def processNode(id): """Check whether the element with the given id has major parents that need to be added to the browser's tree. If such a parent is found, update cDict and toplevel and return True. """ result = False for pid in levels.real[id].parents: if pid in cDict: # We've already added this parent result = True cDict[pid].append(id) toplevel.discard(id) elif elids is None or pid in elids: # This parent belongs to the direct search result result = True toplevel.discard(id) elif processNode(pid): # We must add this parent, because it has a major ancestor. result = True cDict[pid].append(id) toplevel.discard(id) elif levels.real[pid].type.major: # This is a major parent. Add it to toplevel result = True cDict[pid].append(id) toplevel.discard(id) toplevel.add(pid) return result for id in list(toplevel): # copy! processNode(id) def createWrapper(id): """Create a wrapper to be inserted in the browser. If the wrapper should contain all of its element's contents, create a BrowserWrapper, that will load the contents """ element = levels.real[id] if id in cDict: # wrapper should contain only a part of its element's contents wrapper = Wrapper(element) wrapper.setContents([createWrapper(cid) for cid in cDict[id]]) return wrapper elif element.isFile() or len(element.contents) == 0: return Wrapper(element) else: return bnodes.BrowserWrapper(element) # a wrapper that will load all its contents when needed contents = [createWrapper(id) for id in toplevel] def sortFunction(wrapper): """Intelligent sort: sort albums by their date, everything else by name.""" element = wrapper.element date = 0 if element.isContainer() and element.type == elements.ContainerType.Album: dateTag = tags.get("date") if dateTag.type == tags.TYPE_DATE and dateTag in element.tags: date = -element.tags[dateTag][0].toSql() # minus leads to descending sort return (date, element.getTitle(neverShowIds=True)) contents.sort(key=sortFunction) return contents
def build(self, layerIndex, domain, elids, matchingTags): # 1. Get toplevel nodes. if elids is None: toplevel = set(db.query(""" SELECT id FROM {p}elements WHERE domain=? AND id NOT IN (SELECT element_id FROM {p}contents) """, domain.id).getSingleColumn()) if len(toplevel) == 0: return [] elif len(elids) > 0: toplevel = set(elids) toplevel.difference_update(db.query( "SELECT element_id FROM {p}contents WHERE container_id IN ({elids})", elids=db.csList(elids)).getSingleColumn()) else: return [] # Shortcut: For very small result sets simply use a container tree if len(toplevel) <= 5: return _buildContainerTree(domain, elids) # 2. Check whether a VariousNode is necessary. # (that is, some toplevel nodes don't have a tag from self.tagList) # We do this so early because 'toplevel' will be enlarged in the next step. tagFilter = db.csIdList(self.tagList) idFilter = db.csList(toplevel) variousNodeElements = list(db.query(""" SELECT el.id FROM {p}elements AS el LEFT JOIN {p}tags AS t ON el.id = t.element_id AND t.tag_id IN ({tagFilter}) WHERE domain={domain} AND el.id IN ({idFilter}) AND t.value_id IS NULL LIMIT 1 """, tagFilter=tagFilter, idFilter=idFilter, domain=domain.id).getSingleColumn()) # 3. Add contents of permeable nodes to 'toplevel', as long as they are in the search result. # Tag values in these nodes should get a TagNode even if # they don't appear in an actual toplevel node. new = toplevel while len(new): new = set(db.query(""" SELECT c.element_id FROM {p}contents AS c JOIN {p}elements AS el ON c.container_id = el.id WHERE el.type IN ({collection},{container}) AND el.id IN ({parents})""", collection=elements.ContainerType.Collection.value, container=elements.ContainerType.Container.value, parents=db.csList(new)).getSingleColumn()) # Restrict to search result. If the node's value only appears in contents of a permeable # node in the search result and these contents are not in the result themselves, # we would create an empty TagNode. if elids is not None: new.intersection_update(elids) toplevel.update(new) # 4. Create a TagNode for each tag value that appears in 'toplevel' # Make sure to use as single TagNode for equal values in different tags nodes = collections.defaultdict(functools.partial(bnodes.TagNode, layerIndex)) idFilter = db.csList(toplevel) result = db.query(""" SELECT DISTINCT t.tag_id, v.id, v.value, v.hide, v.sort_value FROM {p}tags AS t JOIN {p}values_varchar AS v ON t.tag_id = v.tag_id AND t.value_id = v.id WHERE t.tag_id IN ({tagFilter}) AND t.element_id IN ({idFilter}) """, tagFilter=tagFilter, idFilter=idFilter) for tagId, valueId, value, hide, sortValue in result: matching = (tagId, valueId) in matchingTags nodes[value].addTagValue(tagId, valueId, value, hide, sortValue, matching) # 5. Optimize TagNodes (if there are only few of them) if len(nodes) <= 20: # Above we had to use values as keys, here ids are more useful nodes = {tagTuple: node for node in nodes.values() for tagTuple in node.tagIds} # The first task is to find all contents of each TagNode. Note that the last query (to find # TagNodes) only considered toplevel elements. for node in nodes.values(): node.elids = set() if elids is not None: idFilter = "t.element_id IN ({})".format(db.csList(elids)) else: idFilter = '1' result = db.query(""" SELECT DISTINCT tag_id, value_id, element_id FROM {p}tags AS t WHERE t.tag_id IN ({tagFilter}) AND {idFilter} """, tagFilter=tagFilter, idFilter=idFilter) withinMatchingTags = set() for tagId, valueId, elementId in result: if (tagId, valueId) in nodes: node = nodes[(tagId, valueId)] else: continue # this means that this value does not appear in a 'toplevel' node node.elids.add(elementId) if (tagId, valueId) in matchingTags: withinMatchingTags.add(elementId) def checkSuperNode(node, superNode): """Given two nodes where the second contains all contents of the first, return whether the first node may be deleted. As a sideeffect this method may merge *node* into *superNode*. """ # If *node* is completely contained in superNode, delete it. if len(node.elids) < len(superNode.elids): # However, matching nodes must not be deleted in favor of not matching ones. # and visible nodes must not be deleted in favor of a hidden superNode. return not ((node.matching and not superNode.matching) or (not node.hide and superNode.hide)) else: if node.hide == superNode.hide: superNode.merge(node) return True else: return node.hide for k in list(nodes.keys()): node = nodes[k] # Delete nodes whose contents are covered by nodes matching the search query. if not node.matching and node.elids <= withinMatchingTags: del nodes[k] continue # Try to delete (or merge) TagNodes whose contents are contained in (or equal to) another # TagNode for node2 in nodes.values(): if node2 is not node and node.elids <= node2.elids: # node2 is a superNode: if checkSuperNode(node, node2): del nodes[k] break # 6. Create final list of nodes visibleNodes = [node for node in nodes.values() if not node.hide] hiddenNodes = [node for node in nodes.values() if node.hide] visibleNodes.sort(key=lambda node: locale.strxfrm(node.sortValues[0][0])) hiddenNodes.sort(key=lambda node: locale.strxfrm(node.sortValues[0][0])) if len(variousNodeElements) > 0: node = bnodes.VariousNode(layerIndex, self.tagList) visibleNodes.append(node) if len(hiddenNodes) > 0: # If hidden nodes are present this layer needs two actual levels in the tree structure # Since this interferes with the algorithm to determine the layer of a node, we have to store # that layer index. See BrowserModel._getLayerIndex for node in hiddenNodes: node.layer = self visibleNodes.append(bnodes.HiddenValuesNode(hiddenNodes)) return visibleNodes
def loadFromDb(self, idList, level=None): """Load elements specified by *idList* from the database into *level* which defaults to the real level.""" if level is None: level = self if len(idList) == 0: # queries will fail otherwise return [] csIdList = db.csList(idList) # bare elements result = db.query(""" SELECT el.domain, el.id, el.file, el.type, f.url, f.length FROM {0}elements AS el LEFT JOIN {0}files AS f ON el.id = f.element_id WHERE el.id IN ({1}) """.format(db.prefix, csIdList)) for domainId, id, file, elementType, url, length in result: _dbIds.add(id) if file: level.elements[id] = elements.File( domains.domainById(domainId), level, id, url=URL(url), length=length, type=elements.ContainerType(elementType)) else: level.elements[id] = elements.Container( domains.domainById(domainId), level, id, type=elements.ContainerType(elementType)) # contents result = db.query(""" SELECT el.id, c.position, c.element_id FROM {0}elements AS el JOIN {0}contents AS c ON el.id = c.container_id WHERE el.id IN ({1}) ORDER BY position """.format(db.prefix, csIdList)) for id, pos, contentId in result: level.elements[id].contents.insert(pos, contentId) # parents result = db.query(""" SELECT el.id, c.container_id FROM {0}elements AS el JOIN {0}contents AS c ON el.id = c.element_id WHERE el.id IN ({1}) """.format(db.prefix, csIdList)) for id, contentId in result: level.elements[id].parents.append(contentId) # tags result = db.query(""" SELECT el.id, t.tag_id, t.value_id FROM {0}elements AS el JOIN {0}tags AS t ON el.id = t.element_id WHERE el.id IN ({1}) """.format(db.prefix, csIdList)) for id, tagId, valueId in result: tag = tags.get(tagId) level.elements[id].tags.add(tag, db.tags.value(tag, valueId)) # flags result = db.query(""" SELECT el.id, f.flag_id FROM {0}elements AS el JOIN {0}flags AS f ON el.id = f.element_id WHERE el.id IN ({1}) """.format(db.prefix, csIdList)) for id, flagId in result: level.elements[id].flags.append(flags.get(flagId)) # stickers result = db.query(""" SELECT element_id, type, data FROM {}stickers WHERE element_id IN ({}) ORDER BY element_id, type, sort """.format(db.prefix, csIdList)) # This is a bit complicated because the stickers should be stored in tuples, not lists # Changing the lists would break undo/redo #TODO: is this really necessary? current = None buffer = [] for (id, type, sticker) in result: if current is None: current = (id, type) elif current != (id, type): level.elements[current[0]].stickers[current[1]] = tuple(buffer) current = (id, type) buffer = [] element = level.elements[id] if element.stickers is None: element.stickers = {} buffer.append(sticker) if current is not None: level.elements[current[0]].stickers[current[1]] = tuple(buffer) try: return [self.elements[id] for id in idList] except KeyError: # probably some ids were not contained in the database raise levels.ElementGetError( self, [id for id in idList if id not in self])