Start bringing everything together.
This commit is contained in:
@@ -206,7 +206,7 @@ class MenuBar(QtWidgets.QMenuBar):
|
||||
viewMenu.addAction(resolutionFindAction)
|
||||
viewMenu.addSeparator()
|
||||
|
||||
rearrangeGraphAction = QtGui.QAction("Rearrange Graph",
|
||||
rearrangeGraphAction = QtGui.QAction("Rearrange Canvas Graph",
|
||||
self,
|
||||
statusTip="Rearrange the nodes on the current Canvas to a default "
|
||||
"configuration according to the currently configured graphing "
|
||||
@@ -214,7 +214,7 @@ class MenuBar(QtWidgets.QMenuBar):
|
||||
triggered=self.rearrangeGraph)
|
||||
viewMenu.addAction(rearrangeGraphAction)
|
||||
|
||||
rearrangeAsTimelineAction = QtGui.QAction("Rearrange Graph as Timeline",
|
||||
rearrangeAsTimelineAction = QtGui.QAction("Rearrange Canvas Graph as Timeline",
|
||||
self,
|
||||
statusTip="Rearrange the nodes on the current Canvas to a "
|
||||
"Left-to-Right half-tree according to the entities' "
|
||||
@@ -288,6 +288,20 @@ class MenuBar(QtWidgets.QMenuBar):
|
||||
triggered=self.entityNotesToTextFile)
|
||||
nodeOperationsMenu.addAction(notesToTextFilesAction)
|
||||
|
||||
projectMenu = self.addMenu("Project Operations")
|
||||
projectMenu.setStyleSheet(Stylesheets.MENUS_STYLESHEET_2)
|
||||
generateReportAction = QtGui.QAction("Generate Report",
|
||||
self,
|
||||
statusTip="Generate a report from the set of currently selected nodes.",
|
||||
triggered=self.generateReport)
|
||||
projectMenu.addAction(generateReportAction)
|
||||
|
||||
queryAction = QtGui.QAction("Query Wizard",
|
||||
self,
|
||||
statusTip="Run LQL Queries.",
|
||||
triggered=self.queryWizard)
|
||||
projectMenu.addAction(queryAction)
|
||||
|
||||
modulesMenu = self.addMenu("Modules")
|
||||
modulesMenu.setStyleSheet(Stylesheets.MENUS_STYLESHEET_2)
|
||||
|
||||
@@ -955,6 +969,12 @@ class MenuBar(QtWidgets.QMenuBar):
|
||||
def rearrangeGraphToTimeLine(self) -> None:
|
||||
self.parent().centralWidget().tabbedPane.getCurrentScene().rearrangeGraphTimeline()
|
||||
|
||||
def generateReport(self):
|
||||
self.parent().generateReport()
|
||||
|
||||
def queryWizard(self):
|
||||
self.parent().launchQueryWizard()
|
||||
|
||||
def downloadWebsites(self) -> None:
|
||||
websiteEntities = []
|
||||
|
||||
|
||||
@@ -93,13 +93,13 @@ class ToolBarOne(QtWidgets.QToolBar):
|
||||
self.addAction(splitEntity)
|
||||
self.insertSeparator(splitEntity)
|
||||
|
||||
generateReport = QtGui.QAction('Generate Report',
|
||||
self,
|
||||
statusTip="Generate Report of selected entities.",
|
||||
triggered=self.generateReports,
|
||||
icon=QtGui.QIcon(self.parent().RESOURCEHANDLER.getIcon('generateReport')))
|
||||
self.addAction(generateReport)
|
||||
self.insertSeparator(generateReport)
|
||||
generateReportAction = QtGui.QAction('Generate Report',
|
||||
self,
|
||||
statusTip="Generate Report from the selected nodes.",
|
||||
triggered=self.generateReport,
|
||||
icon=QtGui.QIcon(self.parent().RESOURCEHANDLER.getIcon('generateReport')))
|
||||
self.addAction(generateReportAction)
|
||||
self.insertSeparator(generateReportAction)
|
||||
|
||||
rearrangeCanvas = QtGui.QAction('Rearrange Canvas',
|
||||
self,
|
||||
@@ -148,5 +148,5 @@ class ToolBarOne(QtWidgets.QToolBar):
|
||||
def splitEntity(self):
|
||||
self.parent().splitEntity()
|
||||
|
||||
def generateReports(self):
|
||||
def generateReport(self):
|
||||
self.parent().generateReport()
|
||||
|
||||
235
Core/LQL.py
235
Core/LQL.py
@@ -1,7 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from typing import Union
|
||||
from uuid import uuid4
|
||||
import re
|
||||
import networkx as nx
|
||||
import string
|
||||
|
||||
from Core.GlobalVariables import non_string_fields
|
||||
|
||||
"""
|
||||
This class handles the backend stuff for the LinkScope Query Language.
|
||||
@@ -31,27 +36,38 @@ class LQLQueryBuilder:
|
||||
|
||||
QUERIES_HISTORY = {}
|
||||
|
||||
databaseSnapshot = None
|
||||
databaseEntities = None
|
||||
allCanvases = None
|
||||
canvasesEntitiesDict = None
|
||||
allEntityFields = None
|
||||
allEntities = None
|
||||
|
||||
def __init__(self, mainWindow):
|
||||
self.mainWindow = mainWindow
|
||||
|
||||
def takeSnapshot(self):
|
||||
self.mainWindow.LENTDB.dbLock.acquire()
|
||||
# Create a copy
|
||||
self.databaseSnapshot = nx.DiGraph(self.mainWindow.LENTDB.database)
|
||||
self.mainWindow.LENTDB.dbLock.release()
|
||||
|
||||
self.databaseEntities = set(self.databaseSnapshot.nodes)
|
||||
|
||||
self.allCanvases = self.getAllCanvasNames()
|
||||
self.allEntityFields, self.allEntities = self.getAllEntitiesAndFields()
|
||||
self.databaseEntities = self.getAllUIDs()
|
||||
self.canvasesEntitiesDict = self.getCanvasesEntitiesDict(self.allCanvases)
|
||||
self.allEntityFields, self.allEntities = self.getAllEntitiesAndFields()
|
||||
|
||||
def getAllEntitiesAndFields(self) -> (set, list):
|
||||
entitiesSnapshot = {entity['uid']: entity for entity in self.mainWindow.LENTDB.getAllEntities()
|
||||
if entity.get('Entity Type') != 'EntityGroup'}
|
||||
entitiesSnapshot = {entity: self.databaseSnapshot.nodes[entity] for entity in self.databaseSnapshot.nodes
|
||||
if self.databaseSnapshot.nodes[entity].get('Entity Type') != 'EntityGroup'}
|
||||
entityFields = set()
|
||||
for entityUID in entitiesSnapshot:
|
||||
entityFields.update(entitiesSnapshot[entityUID].keys())
|
||||
entityFields.update('*')
|
||||
entityFields.remove('Entity Type')
|
||||
entityFields.remove('uid')
|
||||
for field in non_string_fields:
|
||||
entityFields.remove(field)
|
||||
return entityFields, entitiesSnapshot
|
||||
|
||||
def getAllUIDs(self) -> set:
|
||||
return self.mainWindow.LENTDB.getAllEntityUIDs()
|
||||
|
||||
def getAllCanvasNames(self) -> list:
|
||||
canvasNames = list(self.mainWindow.centralWidget().tabbedPane.canvasTabs.keys())
|
||||
canvasNames.append('*')
|
||||
@@ -59,7 +75,9 @@ class LQLQueryBuilder:
|
||||
|
||||
def getEntitiesOnCanvas(self, canvasName: str):
|
||||
try:
|
||||
return set(self.mainWindow.centralWidget().tabbedPane.canvasTabs[canvasName].sceneGraph.nodes)
|
||||
# Ensure that we don't have nodes here that are not present in our database snapshot
|
||||
canvasNodes = set(self.mainWindow.centralWidget().tabbedPane.canvasTabs[canvasName].sceneGraph.nodes)
|
||||
return canvasNodes.intersection(self.databaseEntities)
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
@@ -79,18 +97,21 @@ class LQLQueryBuilder:
|
||||
if '*' in selectValue:
|
||||
# No need to remove the '*'. Could cause errors if that's a field name (even though it is bad practice).
|
||||
return self.allEntityFields
|
||||
return [entityField for entityField in selectValue if entityField in self.allEntityFields]
|
||||
return set([entityField for entityField in selectValue if entityField in self.allEntityFields])
|
||||
else:
|
||||
try:
|
||||
clauseValue = re.compile(selectValue)
|
||||
return [entityField for entityField in self.allEntityFields if clauseValue.match(entityField)]
|
||||
return set([entityField for entityField in self.allEntityFields if clauseValue.match(entityField)])
|
||||
except re.error:
|
||||
return []
|
||||
return set()
|
||||
|
||||
def parseSource(self, sourceClause: str, sourceValues: Union[None, list]):
|
||||
def parseSource(self, sourceClause: str, sourceValues: Union[None, list]) -> set:
|
||||
"""
|
||||
sourceValues:
|
||||
[[("AND"|"OR"|None), ("CANVAS"|"RCANVAS"), (True|False), <User Input>], ...]
|
||||
[[("AND" | "OR" | None), ("CANVAS" | "RCANVAS"), (True | False), <User Input>], ...]
|
||||
OR
|
||||
None
|
||||
if sourceClause == "FROMDB"
|
||||
"""
|
||||
if sourceClause == "FROMDB":
|
||||
return self.databaseEntities
|
||||
@@ -130,20 +151,21 @@ class LQLQueryBuilder:
|
||||
|
||||
return resultEntitySet
|
||||
|
||||
def parseConditions(self, conditionClauses: Union[None, list], entitiesPool):
|
||||
def parseConditions(self, conditionClauses: Union[None, list], entitiesPool) -> set:
|
||||
"""
|
||||
conditionClauses:
|
||||
[[("AND", "OR", None), ("VC"|"GC"), (True | False), conditionValue], ...]
|
||||
[[("AND" | "OR" | None), ("VC" | "GC"), (True | False), conditionValue], ...]
|
||||
|
||||
conditionValue:
|
||||
if VC:
|
||||
[("ATTRIBUTE" | "RATTRIBUTE"), <User Input>,
|
||||
("EQ" | "CONTAINS" | "STARTSWITH" | "ENDSWITH" | "RMATCH"), <User Input>]
|
||||
if GC:
|
||||
[("CHILDOF" <ENTITY> | "PARENTOF" <ENTITY> |
|
||||
[("CHILDOF" <ENTITY> | "DESCENDANTOF " <ENTITY> |
|
||||
"PARENTOF" <ENTITY> | "ANCESTOROF " <ENTITY> |
|
||||
"NUMCHILDREN" (" < " | " <= " | " > " | " >= " | " == ") <DIGITS> |
|
||||
"NUMPARENTS" (" < " | " <= " | " > " | " >= " | " == ") <DIGITS> |
|
||||
"PATHTO" <ENTITY> | "ISOLATED" | "ISROOT" | "ISLEAF")]
|
||||
"CONNECTEDTO" <ENTITY> | "ISOLATED" | "ISROOT" | "ISLEAF")]
|
||||
"""
|
||||
|
||||
self.allEntities = {uid: self.allEntities[uid] for uid in self.allEntities if uid in entitiesPool}
|
||||
@@ -183,6 +205,12 @@ class LQLQueryBuilder:
|
||||
elif conditionClause[1] == "GC":
|
||||
pass
|
||||
|
||||
uidsToRemove = set(self.allEntities).difference(uidsToSelect)
|
||||
for entity in uidsToRemove:
|
||||
self.allEntities.pop(entity, None)
|
||||
|
||||
return uidsToSelect
|
||||
|
||||
def canvasOr(self, canvasSetA: set, canvasSetB: set):
|
||||
return canvasSetA.union(canvasSetB)
|
||||
|
||||
@@ -241,28 +269,179 @@ class LQLQueryBuilder:
|
||||
return returnVal
|
||||
|
||||
def checkChildOf(self, valueA: str, valueB: str):
|
||||
return False
|
||||
return self.databaseSnapshot.has_successor(valueA, valueB)
|
||||
|
||||
def checkSuccessorOf(self, valueA: str, valueB: str):
|
||||
def checkDescendantOf(self, valueA: str, valueB: str):
|
||||
try:
|
||||
if valueB in nx.descendants(self.databaseSnapshot, valueA):
|
||||
return True
|
||||
except nx.NetworkXError:
|
||||
pass
|
||||
return False
|
||||
|
||||
def checkParentOf(self, valueA: str, valueB: str):
|
||||
return self.databaseSnapshot.has_predecessor(valueA, valueB)
|
||||
|
||||
def checkAncestorOf(self, valueA: str, valueB: str):
|
||||
try:
|
||||
if valueB in nx.ancestors(self.databaseSnapshot, valueA):
|
||||
return True
|
||||
except nx.NetworkXError:
|
||||
pass
|
||||
return False
|
||||
|
||||
def checkPredecessorOf(self, valueA: str, valueB: str):
|
||||
def checkNumChildren(self, valueA: str, valueB: str, valueC: int):
|
||||
numChildren = len(list(self.databaseSnapshot.successors(valueA)))
|
||||
returnValue = False
|
||||
if valueB == "<":
|
||||
if numChildren < valueC:
|
||||
returnValue = True
|
||||
elif valueB == "<=":
|
||||
if numChildren <= valueC:
|
||||
returnValue = True
|
||||
elif valueB == ">":
|
||||
if numChildren > valueC:
|
||||
returnValue = True
|
||||
elif valueB == ">=":
|
||||
if numChildren >= valueC:
|
||||
returnValue = True
|
||||
elif valueB == "==":
|
||||
if numChildren == valueC:
|
||||
returnValue = True
|
||||
return returnValue
|
||||
|
||||
def checkNumParents(self, valueA: str, valueB: str, valueC: int):
|
||||
numParents = len(list(self.databaseSnapshot.predecessors(valueA)))
|
||||
returnValue = False
|
||||
if valueB == "<":
|
||||
if numParents < valueC:
|
||||
returnValue = True
|
||||
elif valueB == "<=":
|
||||
if numParents <= valueC:
|
||||
returnValue = True
|
||||
elif valueB == ">":
|
||||
if numParents > valueC:
|
||||
returnValue = True
|
||||
elif valueB == ">=":
|
||||
if numParents >= valueC:
|
||||
returnValue = True
|
||||
elif valueB == "==":
|
||||
if numParents == valueC:
|
||||
returnValue = True
|
||||
return returnValue
|
||||
|
||||
def checkConnectedTo(self, valueA: str, valueB: str):
|
||||
try:
|
||||
if nx.has_path(self.databaseSnapshot, valueA, valueB):
|
||||
return True
|
||||
except nx.NetworkXError:
|
||||
pass
|
||||
return False
|
||||
|
||||
def checkIsolated(self, valueA: str):
|
||||
try:
|
||||
if valueA in self.databaseSnapshot.nodes and nx.is_isolate(self.databaseSnapshot, valueA):
|
||||
return True
|
||||
except nx.NetworkXError:
|
||||
pass
|
||||
return False
|
||||
|
||||
def checkIsRoot(self, valueA: str):
|
||||
try:
|
||||
if len(self.databaseSnapshot.in_edges(valueA)) == 0:
|
||||
return True
|
||||
except nx.NetworkXError:
|
||||
pass
|
||||
return False
|
||||
|
||||
def checkIsLeaf(self, valueA: str):
|
||||
try:
|
||||
if len(self.databaseSnapshot.out_edges(valueA)) == 0:
|
||||
return True
|
||||
except nx.NetworkXError:
|
||||
pass
|
||||
return False
|
||||
|
||||
def checkGCHelper(self, checkType: str, isNot: bool, args: list):
|
||||
returnVal = False
|
||||
if checkType == "CHILDOF":
|
||||
returnVal = self.checkChildOf(*args)
|
||||
elif checkType == "DESCENDANTOF":
|
||||
returnVal = self.checkDescendantOf(*args)
|
||||
elif checkType == "PARENTOF":
|
||||
returnVal = self.checkParentOf(*args)
|
||||
elif checkType == "ANCESTOROF":
|
||||
returnVal = self.checkAncestorOf(*args)
|
||||
elif checkType == "NUMCHILDREN":
|
||||
returnVal = self.checkNumChildren(*args)
|
||||
elif checkType == "NUMPARENTS":
|
||||
returnVal = self.checkNumParents(*args)
|
||||
elif checkType == "CONNECTEDTO":
|
||||
returnVal = self.checkConnectedTo(*args)
|
||||
elif checkType == "ISOLATED":
|
||||
returnVal = self.checkIsolated(*args)
|
||||
elif checkType == "ISROOT":
|
||||
returnVal = self.checkIsRoot(*args)
|
||||
elif checkType == "ISLEAF":
|
||||
returnVal = self.checkIsLeaf(*args)
|
||||
if isNot:
|
||||
return not returnVal
|
||||
return returnVal
|
||||
|
||||
def parseQuery(self, selectClause: str, selectValue: Union[str, list], sourceClause: str,
|
||||
sourceValues: Union[None, list], conditionClauses: Union[None, list]):
|
||||
pass
|
||||
def modifyNumify(self, valueA: str):
|
||||
# Get the first number that shows up.
|
||||
tempString = valueA.replace(',', '.') # Making sure that floats are expressed the right way.
|
||||
count = 0
|
||||
for c in tempString:
|
||||
if c not in string.digits:
|
||||
count += 1
|
||||
else:
|
||||
break
|
||||
|
||||
count2 = 0
|
||||
for c in tempString[count:]:
|
||||
if c in string.digits or c == '.':
|
||||
count2 += 1
|
||||
else:
|
||||
break
|
||||
|
||||
return float(tempString[count:count + count2])
|
||||
|
||||
def parseModify(self, resultsToModify: (set, set), update: Union[bool, None] = None,
|
||||
modifyQueries: Union[list, None] = None) -> (set, set):
|
||||
"""
|
||||
modifyQueries:
|
||||
[[("MODIFY" | "RMODIFY"), <User Input>, ("NUMIFY" | "UPPERCASE" | "LOWERCASE")], ...]
|
||||
"""
|
||||
|
||||
if modifyQueries is None:
|
||||
return resultsToModify
|
||||
|
||||
modifiedEntities = {}
|
||||
|
||||
for modification in modifyQueries:
|
||||
pass # TODO
|
||||
|
||||
def parseQuery(self, selectClause: str, selectValue: Union[str, list], sourceClause: str,
|
||||
sourceValues: Union[None, list], conditionClauses: Union[None, list],
|
||||
modifyUpdate: Union[bool, None] = None,
|
||||
modifyQueries: Union[list, None] = None) -> Union[(set, set), None]:
|
||||
|
||||
if self.databaseSnapshot is None:
|
||||
return None
|
||||
|
||||
returnValue = None
|
||||
fieldsToSelect = self.parseSelect(selectClause, selectValue)
|
||||
if fieldsToSelect:
|
||||
entitiesToConsider = self.parseSource(sourceClause, sourceValues)
|
||||
if entitiesToConsider:
|
||||
finalSetOfUIDs = self.parseConditions(conditionClauses, entitiesToConsider)
|
||||
if finalSetOfUIDs:
|
||||
if not modifyQueries:
|
||||
returnValue = (finalSetOfUIDs, fieldsToSelect)
|
||||
|
||||
queryUID = str(uuid4())
|
||||
self.QUERIES_HISTORY[queryUID] = (selectClause, selectValue, sourceClause, sourceValues, conditionClauses)
|
||||
|
||||
return returnValue
|
||||
|
||||
def parseModify(self):
|
||||
pass
|
||||
|
||||
54
LinkScope.py
54
LinkScope.py
@@ -35,6 +35,7 @@ from Core.Interface import ToolBarOne
|
||||
from Core.Interface import MenuBar
|
||||
from Core.Interface import Stylesheets
|
||||
from Core.Interface.Entity import BaseNode, BaseConnector, GroupNode
|
||||
from Core.LQL import LQLQueryBuilder
|
||||
from Core.PathHelper import is_path_exists_or_creatable_portable
|
||||
|
||||
|
||||
@@ -733,6 +734,10 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
for scene in allScenesWithNode:
|
||||
scene.rearrangeGraph()
|
||||
|
||||
def launchQueryWizard(self):
|
||||
queryWizard = QueryBuilderWizard(self)
|
||||
queryWizard.exec()
|
||||
|
||||
def handleGroupNodeUpdateAfterEntityDeletion(self, entityUID) -> None:
|
||||
for canvas in self.centralWidget().tabbedPane.canvasTabs:
|
||||
self.centralWidget().tabbedPane.canvasTabs[canvas].cleanDeletedNodeFromGroupsIfExists(entityUID)
|
||||
@@ -1796,6 +1801,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
self.URLMANAGER = URLManager.URLManager(self)
|
||||
self.RESOLUTIONMANAGER = ResolutionManager.ResolutionManager(self, self.MESSAGEHANDLER)
|
||||
self.FCOM = FrontendCommunicationsHandler.CommunicationsHandler(self)
|
||||
self.LQLWIZARD = LQLQueryBuilder(self)
|
||||
|
||||
# Have the project auto-save on regular intervals by default.
|
||||
self.saveTimer = QtCore.QTimer(self)
|
||||
@@ -3636,6 +3642,54 @@ class SplitEntitiesDialog(QtWidgets.QDialog):
|
||||
super(SplitEntitiesDialog, self).accept()
|
||||
|
||||
|
||||
class QueryBuilderWizard(QtWidgets.QDialog):
|
||||
|
||||
def __init__(self, mainWindowObject: MainWindow):
|
||||
super(QueryBuilderWizard, self).__init__()
|
||||
self.setModal(True)
|
||||
self.setWindowTitle('LQL Query Wizard')
|
||||
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
|
||||
dialogLayout = QtWidgets.QGridLayout()
|
||||
self.setLayout(dialogLayout)
|
||||
|
||||
mainWindowObject.LQLWIZARD.takeSnapshot()
|
||||
|
||||
|
||||
class QueryResultsViewer(QtWidgets.QDialog):
|
||||
|
||||
def __init__(self, entitiesDict: dict, selectedUIDs: set, selectedFields: set):
|
||||
super(QueryResultsViewer, self).__init__()
|
||||
self.setModal(True)
|
||||
self.setWindowTitle('Query Results')
|
||||
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
|
||||
dialogLayout = QtWidgets.QGridLayout()
|
||||
self.setLayout(dialogLayout)
|
||||
|
||||
self.resultsTable = QtWidgets.QTableWidget()
|
||||
headerFields = list(selectedFields)
|
||||
try:
|
||||
headerFields.remove('uid')
|
||||
except ValueError:
|
||||
pass
|
||||
headerFields.insert(0, 'uid')
|
||||
self.resultsTable.setHorizontalHeaderLabels(headerFields)
|
||||
|
||||
count = 0
|
||||
for uid in selectedUIDs:
|
||||
self.resultsTable.insertRow(count)
|
||||
for index, field in enumerate(headerFields):
|
||||
self.resultsTable.setItem(count, index, entitiesDict[uid][field]) # TODO Check that this works
|
||||
count += 1
|
||||
|
||||
closeButton = QtWidgets.QPushButton('Close')
|
||||
closeButton.clicked.connect(self.accept)
|
||||
exportButton = QtWidgets.QPushButton('Export')
|
||||
exportButton.clicked.connect(self.exportData)
|
||||
|
||||
def exportData(self):
|
||||
pass # TODO - Save dialog, write csv file.
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Create a graphical application
|
||||
application = QtWidgets.QApplication(sys.argv)
|
||||
|
||||
Reference in New Issue
Block a user