Start bringing everything together.

This commit is contained in:
AccentuSoft
2022-05-31 02:50:02 +03:00
parent 0a52862baa
commit 2c718a8543
4 changed files with 291 additions and 38 deletions

View File

@@ -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 = []

View File

@@ -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()

View File

@@ -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

View File

@@ -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)