Major refactoring & Minor bug fixes
This commit is contained in:
465
Core/EntityDB.py
465
Core/EntityDB.py
@@ -29,42 +29,37 @@ class EntitiesDB:
|
||||
"""
|
||||
Load DiGraph from LinkScope Database file - msgpack dumped object.
|
||||
"""
|
||||
self.dbLock.acquire()
|
||||
if self.database is not None:
|
||||
self.save()
|
||||
databaseFile = Path(self.mainWindow.SETTINGS.value("Project/FilesDir")).joinpath("LocalEntitiesDB.lsdb")
|
||||
self.messageHandler.debug('Opening Database at: ' + str(databaseFile))
|
||||
try:
|
||||
dbFile = open(databaseFile, "rb")
|
||||
self.database = self.mainWindow.RESOURCEHANDLER.reconstructGraphFullFromFile(load(dbFile))
|
||||
dbFile.close()
|
||||
self.messageHandler.info('Loaded Local Entities Database.')
|
||||
except FileNotFoundError:
|
||||
self.messageHandler.info('Creating new Local Entities Database.')
|
||||
self.database = nx.DiGraph()
|
||||
except Exception as exc:
|
||||
self.messageHandler.error('Cannot parse Database: ' + str(exc) + "\nCreating new Local Entities Database.",
|
||||
popUp=True)
|
||||
self.database = nx.DiGraph()
|
||||
finally:
|
||||
self.dbLock.release()
|
||||
with self.dbLock:
|
||||
if self.database is not None:
|
||||
self.save()
|
||||
databaseFile = Path(self.mainWindow.SETTINGS.value("Project/FilesDir")).joinpath("LocalEntitiesDB.lsdb")
|
||||
self.messageHandler.debug(f'Opening Database at: {str(databaseFile)}')
|
||||
try:
|
||||
with open(databaseFile, "rb") as dbFile:
|
||||
self.database = self.mainWindow.RESOURCEHANDLER.reconstructGraphFullFromFile(load(dbFile))
|
||||
self.messageHandler.info('Loaded Local Entities Database.')
|
||||
except FileNotFoundError:
|
||||
self.messageHandler.info('Creating new Local Entities Database.')
|
||||
self.database = nx.DiGraph()
|
||||
except Exception as exc:
|
||||
self.messageHandler.error(f'Cannot parse Database: {str(exc)}\nCreating new Local Entities Database.',
|
||||
popUp=True)
|
||||
self.database = nx.DiGraph()
|
||||
|
||||
def resetTimeline(self) -> None:
|
||||
"""
|
||||
Reset the timeline on dockBarThree to reflect the current state of the database.
|
||||
"""
|
||||
self.dbLock.acquire()
|
||||
if self.database is not None:
|
||||
self.mainWindow.resetTimeline(self.database)
|
||||
self.dbLock.release()
|
||||
with self.dbLock:
|
||||
if self.database is not None:
|
||||
self.mainWindow.resetTimeline(self.database)
|
||||
|
||||
def updateTimeline(self, node, added: bool, updateGraph: bool = True) -> None:
|
||||
"""
|
||||
Update the timeline on dockBarThree to reflect the newest change of the database.
|
||||
"""
|
||||
self.dbLock.acquire()
|
||||
self.mainWindow.updateTimeline(node, added, updateGraph)
|
||||
self.dbLock.release()
|
||||
with self.dbLock:
|
||||
self.mainWindow.updateTimeline(node, added, updateGraph)
|
||||
|
||||
def save(self) -> None:
|
||||
"""
|
||||
@@ -75,41 +70,38 @@ class EntitiesDB:
|
||||
databaseFile = Path(self.mainWindow.SETTINGS.value("Project/FilesDir")).joinpath("LocalEntitiesDB.lsdb")
|
||||
if databaseFile is None:
|
||||
raise ValueError('Database File is None, cannot save database.')
|
||||
self.dbLock.acquire()
|
||||
tmpSavePath = databaseFile.with_suffix(databaseFile.suffix + '.tmp')
|
||||
dbFile = open(tmpSavePath, "wb")
|
||||
dump(self.mainWindow.RESOURCEHANDLER.deconstructGraphForFileDump(self.database), dbFile)
|
||||
dbFile.close()
|
||||
move(tmpSavePath, databaseFile)
|
||||
self.messageHandler.info('Database Saved.')
|
||||
self.dbLock.release()
|
||||
with self.dbLock:
|
||||
tmpSavePath = databaseFile.with_suffix(f'{databaseFile.suffix}.tmp')
|
||||
with open(tmpSavePath, "wb") as dbFile:
|
||||
dump(self.mainWindow.RESOURCEHANDLER.deconstructGraphForFileDump(self.database), dbFile)
|
||||
move(tmpSavePath, databaseFile)
|
||||
self.messageHandler.info('Database Saved.')
|
||||
|
||||
def addEntity(self, entJson: dict, fromServer: bool = False, updateTimeline: bool = True) -> Union[dict, None]:
|
||||
"""
|
||||
Adds the entity represented by the json dictionary to the database.
|
||||
"""
|
||||
self.dbLock.acquire()
|
||||
returnValue = None
|
||||
with self.dbLock:
|
||||
returnValue = None
|
||||
|
||||
# Check if we're overwriting an existing entity
|
||||
exists = None
|
||||
if entJson.get('uid') is not None:
|
||||
exists = self.getEntityNoLock(entJson.get('uid'))
|
||||
# Check if we're overwriting an existing entity
|
||||
exists = None
|
||||
if entJson.get('uid') is not None:
|
||||
exists = self.getEntityNoLock(entJson.get('uid'))
|
||||
|
||||
entity = self.resourceHandler.getEntityJson(
|
||||
entJson.get('Entity Type'),
|
||||
entJson)
|
||||
entity = self.resourceHandler.getEntityJson(
|
||||
entJson.get('Entity Type'),
|
||||
entJson)
|
||||
|
||||
if entity is None:
|
||||
return returnValue
|
||||
# Use uid as key. Code is holdover from the time when primary field == uid.
|
||||
self.database.add_node(entity['uid'], **entity)
|
||||
returnValue = entity
|
||||
if exists:
|
||||
# Update canvases if the node already exists.
|
||||
self.mainWindow.updateEntityNodeLabelsOnCanvases(entity['uid'], entity[list(entity)[1]])
|
||||
|
||||
if entity is None:
|
||||
self.dbLock.release()
|
||||
return returnValue
|
||||
# Use uid as key. Code is holdover from the time when primary field == uid.
|
||||
self.database.add_node(entity['uid'], **entity)
|
||||
returnValue = entity
|
||||
if exists:
|
||||
# Update canvases if the node already exists.
|
||||
self.mainWindow.updateEntityNodeLabelsOnCanvases(entity['uid'], entity[list(entity)[1]])
|
||||
self.dbLock.release()
|
||||
if not fromServer:
|
||||
self.mainWindow.sendLocalDatabaseUpdateToServer(entity, 1)
|
||||
self.mainWindow.populateEntitiesWidget(returnValue, add=True)
|
||||
@@ -123,32 +115,31 @@ class EntitiesDB:
|
||||
return returnValue
|
||||
|
||||
def addEntities(self, entitiesJsonList: Union[list, set, tuple], fromServer: bool = False) -> list:
|
||||
self.dbLock.acquire()
|
||||
returnValue = []
|
||||
with self.dbLock:
|
||||
returnValue = []
|
||||
|
||||
for entJson in entitiesJsonList:
|
||||
# Check if we're overwriting an existing entity
|
||||
exists = None
|
||||
if entJson.get('uid') is not None:
|
||||
exists = self.getEntityNoLock(entJson.get('uid'))
|
||||
for entJson in entitiesJsonList:
|
||||
# Check if we're overwriting an existing entity
|
||||
exists = None
|
||||
if entJson.get('uid') is not None:
|
||||
exists = self.getEntityNoLock(entJson.get('uid'))
|
||||
|
||||
entity = self.resourceHandler.getEntityJson(
|
||||
entJson.get('Entity Type'),
|
||||
entJson)
|
||||
entity = self.resourceHandler.getEntityJson(
|
||||
entJson.get('Entity Type'),
|
||||
entJson)
|
||||
|
||||
if entity is None:
|
||||
continue
|
||||
# Use uid as key. Code is holdover from the time when primary field == uid.
|
||||
self.database.add_node(entity['uid'], **entity)
|
||||
returnValue.append(entity)
|
||||
if exists:
|
||||
# Update canvases if the node already exists.
|
||||
self.mainWindow.updateEntityNodeLabelsOnCanvases(entity['uid'], entity[list(entity)[1]])
|
||||
if not fromServer:
|
||||
self.mainWindow.sendLocalDatabaseUpdateToServer(entity, 1)
|
||||
self.mainWindow.populateEntitiesWidget(entity, add=True)
|
||||
if entity is None:
|
||||
continue
|
||||
# Use uid as key. Code is holdover from the time when primary field == uid.
|
||||
self.database.add_node(entity['uid'], **entity)
|
||||
returnValue.append(entity)
|
||||
if exists:
|
||||
# Update canvases if the node already exists.
|
||||
self.mainWindow.updateEntityNodeLabelsOnCanvases(entity['uid'], entity[list(entity)[1]])
|
||||
if not fromServer:
|
||||
self.mainWindow.sendLocalDatabaseUpdateToServer(entity, 1)
|
||||
self.mainWindow.populateEntitiesWidget(entity, add=True)
|
||||
|
||||
self.dbLock.release()
|
||||
self.resetTimeline()
|
||||
|
||||
return returnValue
|
||||
@@ -162,42 +153,40 @@ class EntitiesDB:
|
||||
:param fromServer:
|
||||
:return:
|
||||
"""
|
||||
self.dbLock.acquire()
|
||||
exists = self.isLinkNoLock(linkJson['uid'])
|
||||
link = self.resourceHandler.getLinkJson(linkJson)
|
||||
if link is None:
|
||||
# This can technically be caused by a race condition if the user
|
||||
# either tries really hard or gets really unlucky.
|
||||
# Caused by deleting a node faster than the link can be created.
|
||||
self.messageHandler.error("Attempted to add Link with "
|
||||
"no uid to database.", popUp=True)
|
||||
self.dbLock.release()
|
||||
return None
|
||||
else:
|
||||
linkUID = link['uid']
|
||||
if exists:
|
||||
newRes = link.get('Resolution')
|
||||
newNotes = link.get('Notes')
|
||||
if newRes and newRes != exists['Resolution']:
|
||||
if overwrite:
|
||||
link['Resolution'] = newRes
|
||||
else:
|
||||
link['Resolution'] = exists['Resolution'] + ' | ' + newRes
|
||||
if newNotes and newNotes != exists['Notes'] and newNotes != 'None':
|
||||
if overwrite:
|
||||
link['Notes'] = str(newNotes)
|
||||
else:
|
||||
link['Notes'] = exists['Notes'] + '\n\n' + str(newNotes)
|
||||
exists.update(link)
|
||||
link.update(exists)
|
||||
# Update canvases if the link already exists.
|
||||
# We can do this before updating the database here because the GUI will be updated only after this
|
||||
# function returns. If we ever execute this function outside the main event loop, we will need
|
||||
# to alter the execution flow.
|
||||
self.mainWindow.updateLinkLabelsOnCanvases(linkUID[0] + linkUID[1], link['Resolution'])
|
||||
self.database.add_edge(linkUID[0], linkUID[1], **link)
|
||||
with self.dbLock:
|
||||
exists = self.isLinkNoLock(linkJson['uid'])
|
||||
link = self.resourceHandler.getLinkJson(linkJson)
|
||||
if link is None:
|
||||
# This can technically be caused by a race condition if the user
|
||||
# either tries really hard or gets really unlucky.
|
||||
# Caused by deleting a node faster than the link can be created.
|
||||
self.messageHandler.error("Attempted to add Link with "
|
||||
"no uid to database.", popUp=True)
|
||||
return None
|
||||
else:
|
||||
linkUID = link['uid']
|
||||
if exists:
|
||||
newRes = link.get('Resolution')
|
||||
newNotes = link.get('Notes')
|
||||
if newRes and newRes != exists['Resolution']:
|
||||
if overwrite:
|
||||
link['Resolution'] = newRes
|
||||
else:
|
||||
link['Resolution'] = f"{exists['Resolution']} | {newRes}"
|
||||
if newNotes and newNotes != exists['Notes'] and newNotes != 'None':
|
||||
if overwrite:
|
||||
link['Notes'] = str(newNotes)
|
||||
else:
|
||||
link['Notes'] = f"{exists['Notes']}\n\n{str(newNotes)}"
|
||||
exists.update(link)
|
||||
link.update(exists)
|
||||
# Update canvases if the link already exists.
|
||||
# We can do this before updating the database here because the GUI will be updated only after this
|
||||
# function returns. If we ever execute this function outside the main event loop, we will need
|
||||
# to alter the execution flow.
|
||||
self.mainWindow.updateLinkLabelsOnCanvases(f"{linkUID[0]}{linkUID[1]}", link['Resolution'])
|
||||
self.database.add_edge(linkUID[0], linkUID[1], **link)
|
||||
|
||||
self.dbLock.release()
|
||||
if not fromServer:
|
||||
if overwrite:
|
||||
self.mainWindow.sendLocalDatabaseUpdateToServer(link, 3)
|
||||
@@ -209,51 +198,41 @@ class EntitiesDB:
|
||||
"""
|
||||
Returns the attributes of the given entity uid as a dict.
|
||||
"""
|
||||
self.dbLock.acquire()
|
||||
returnValue = None
|
||||
try:
|
||||
returnValue = self.database.nodes[uid]
|
||||
except KeyError:
|
||||
self.messageHandler.warning(
|
||||
"Tried to get entity with nonexistent UID: " + uid)
|
||||
finally:
|
||||
self.dbLock.release()
|
||||
return returnValue
|
||||
with self.dbLock:
|
||||
returnValue = None
|
||||
try:
|
||||
returnValue = self.database.nodes[uid]
|
||||
except KeyError:
|
||||
self.messageHandler.warning(f"Tried to get entity with nonexistent UID: {uid}")
|
||||
finally:
|
||||
return returnValue
|
||||
|
||||
def getAllEntities(self) -> Union[None, list]:
|
||||
"""
|
||||
Returns a list containing the Json representation of every entity in the database.
|
||||
"""
|
||||
self.dbLock.acquire()
|
||||
returnValue = None
|
||||
try:
|
||||
returnValue = []
|
||||
for node in self.database.nodes():
|
||||
returnValue += [self.database.nodes[node]]
|
||||
except KeyError:
|
||||
self.messageHandler.error(
|
||||
"Tried to get entity with nonexistent UID.")
|
||||
finally:
|
||||
self.dbLock.release()
|
||||
return returnValue
|
||||
with self.dbLock:
|
||||
returnValue = None
|
||||
try:
|
||||
returnValue = [self.database.nodes[node] for node in self.database.nodes()]
|
||||
except KeyError:
|
||||
self.messageHandler.error("Tried to get entity with nonexistent UID.")
|
||||
finally:
|
||||
return returnValue
|
||||
|
||||
def getAllLinks(self) -> Union[None, list]:
|
||||
"""
|
||||
Returns a list containing the Json representation of every link in the database.
|
||||
:return:
|
||||
"""
|
||||
self.dbLock.acquire()
|
||||
returnValue = None
|
||||
try:
|
||||
returnValue = []
|
||||
for edge in self.database.edges():
|
||||
returnValue += [self.database.edges[edge]]
|
||||
except KeyError:
|
||||
self.messageHandler.error(
|
||||
"Tried to get link with nonexistent UID.")
|
||||
finally:
|
||||
self.dbLock.release()
|
||||
return returnValue
|
||||
with self.dbLock:
|
||||
returnValue = None
|
||||
try:
|
||||
returnValue = [self.database.edges[edge] for edge in self.database.edges()]
|
||||
except KeyError:
|
||||
self.messageHandler.error("Tried to get link with nonexistent UID.")
|
||||
finally:
|
||||
return returnValue
|
||||
|
||||
def getEntityNoLock(self, uid: str) -> Union[None, dict]:
|
||||
"""
|
||||
@@ -274,28 +253,27 @@ class EntitiesDB:
|
||||
"""
|
||||
Returns the attributes of the given link uid as a dict.
|
||||
"""
|
||||
self.dbLock.acquire()
|
||||
returnValue = None
|
||||
try:
|
||||
returnValue = self.database.edges[uid]
|
||||
except KeyError:
|
||||
self.messageHandler.error(
|
||||
"Tried to get link with nonexistent UID.")
|
||||
finally:
|
||||
self.dbLock.release()
|
||||
return returnValue
|
||||
with self.dbLock:
|
||||
returnValue = None
|
||||
try:
|
||||
returnValue = self.database.edges[uid]
|
||||
except KeyError:
|
||||
self.messageHandler.error(
|
||||
"Tried to get link with nonexistent UID.")
|
||||
finally:
|
||||
return returnValue
|
||||
|
||||
def removeEntity(self, uid: str, fromServer=False, updateTimeLine=True) -> None:
|
||||
"""
|
||||
Removes the entity with the given uid, if it exists.
|
||||
"""
|
||||
self.dbLock.acquire()
|
||||
ent = None
|
||||
if self.isNodeNoLock(uid):
|
||||
ent = self.getEntityNoLock(uid)
|
||||
self.mainWindow.populateEntitiesWidget(ent, add=False)
|
||||
self.database.remove_node(uid)
|
||||
self.dbLock.release()
|
||||
with self.dbLock:
|
||||
ent = None
|
||||
if self.isNodeNoLock(uid):
|
||||
ent = self.getEntityNoLock(uid)
|
||||
self.mainWindow.populateEntitiesWidget(ent, add=False)
|
||||
self.database.remove_node(uid)
|
||||
|
||||
if ent is not None:
|
||||
self.mainWindow.handleGroupNodeUpdateAfterEntityDeletion(uid) # Blocking - locks the db.
|
||||
if not fromServer:
|
||||
@@ -308,10 +286,9 @@ class EntitiesDB:
|
||||
Removes the link with the given uid (in string or tuple form),
|
||||
if it exists.
|
||||
"""
|
||||
self.dbLock.acquire()
|
||||
if self.isLinkNoLock(uid):
|
||||
self.database.remove_edge(uid[0], uid[1])
|
||||
self.dbLock.release()
|
||||
with self.dbLock:
|
||||
if self.isLinkNoLock(uid):
|
||||
self.database.remove_edge(uid[0], uid[1])
|
||||
if not fromServer:
|
||||
self.mainWindow.sendLocalDatabaseUpdateToServer({"uid": uid}, 2)
|
||||
|
||||
@@ -319,14 +296,13 @@ class EntitiesDB:
|
||||
"""
|
||||
Checks if an entity with the specified primary attribute exists.
|
||||
"""
|
||||
self.dbLock.acquire()
|
||||
result = False
|
||||
for node in self.database.nodes():
|
||||
details = self.database.nodes[node]
|
||||
if details[list(details)[1]] == primaryAttr:
|
||||
result = True
|
||||
break
|
||||
self.dbLock.release()
|
||||
with self.dbLock:
|
||||
result = False
|
||||
for node in self.database.nodes():
|
||||
details = self.database.nodes[node]
|
||||
if details[list(details)[1]] == primaryAttr:
|
||||
result = True
|
||||
break
|
||||
return result
|
||||
|
||||
def getEntityOfType(self, primaryAttr: str, entityType: str) -> Union[dict, None]:
|
||||
@@ -337,13 +313,12 @@ class EntitiesDB:
|
||||
primaryField = self.resourceHandler.getPrimaryFieldForEntityType(entityType)
|
||||
if primaryField is None:
|
||||
return result
|
||||
self.dbLock.acquire()
|
||||
for node in self.database.nodes():
|
||||
details = self.database.nodes[node]
|
||||
if details['Entity Type'] == entityType and details[primaryField] == primaryAttr:
|
||||
result = dict(details)
|
||||
break
|
||||
self.dbLock.release()
|
||||
with self.dbLock:
|
||||
for node in self.database.nodes():
|
||||
details = self.database.nodes[node]
|
||||
if details['Entity Type'] == entityType and details[primaryField] == primaryAttr:
|
||||
result = dict(details)
|
||||
break
|
||||
return result
|
||||
|
||||
def getLinkIfExists(self, uid) -> Union[None, dict]:
|
||||
@@ -351,36 +326,29 @@ class EntitiesDB:
|
||||
Returns the attributes of the given link uid as a dict.
|
||||
Does not create an error if the link does not exist.
|
||||
"""
|
||||
self.dbLock.acquire()
|
||||
returnValue = None
|
||||
try:
|
||||
returnValue = self.database.edges[uid]
|
||||
except KeyError:
|
||||
pass
|
||||
finally:
|
||||
self.dbLock.release()
|
||||
return returnValue
|
||||
with self.dbLock:
|
||||
returnValue = None
|
||||
try:
|
||||
returnValue = self.database.edges[uid]
|
||||
except KeyError:
|
||||
pass
|
||||
finally:
|
||||
return returnValue
|
||||
|
||||
def getIncomingLinks(self, uid: str):
|
||||
"""
|
||||
Get all incoming edges for the given entity uid (primary attribute).
|
||||
"""
|
||||
self.dbLock.acquire()
|
||||
returnValue = None
|
||||
if self.isNodeNoLock(uid):
|
||||
returnValue = self.database.in_edges(uid)
|
||||
self.dbLock.release()
|
||||
with self.dbLock:
|
||||
returnValue = self.database.in_edges(uid) if self.isNodeNoLock(uid) else None
|
||||
return returnValue
|
||||
|
||||
def getOutgoingLinks(self, uid: str):
|
||||
"""
|
||||
Get all outgoing edges for the given entity uid (primary attribute).
|
||||
"""
|
||||
self.dbLock.acquire()
|
||||
returnValue = None
|
||||
if self.isNodeNoLock(uid):
|
||||
returnValue = self.database.out_edges(uid)
|
||||
self.dbLock.release()
|
||||
with self.dbLock:
|
||||
returnValue = self.database.out_edges(uid) if self.isNodeNoLock(uid) else None
|
||||
return returnValue
|
||||
|
||||
def isNode(self, uid: Union[str, list, tuple]) -> bool:
|
||||
@@ -388,11 +356,8 @@ class EntitiesDB:
|
||||
Returns True if the uid (primary attribute) given exists as
|
||||
an entity, and False otherwise.
|
||||
"""
|
||||
self.dbLock.acquire()
|
||||
returnValue = False
|
||||
if isinstance(uid, str) and self.database.nodes.get(uid) is not None:
|
||||
returnValue = True
|
||||
self.dbLock.release()
|
||||
with self.dbLock:
|
||||
returnValue = isinstance(uid, str) and self.database.nodes.get(uid) is not None
|
||||
return returnValue
|
||||
|
||||
def isNodeNoLock(self, uid: str) -> bool:
|
||||
@@ -402,19 +367,14 @@ class EntitiesDB:
|
||||
|
||||
Used only in this class, as it does not lock.
|
||||
"""
|
||||
if self.database.nodes.get(uid) is not None:
|
||||
return True
|
||||
return False
|
||||
return self.database.nodes.get(uid) is not None
|
||||
|
||||
def isLink(self, uid: Union[str, list, tuple]) -> bool:
|
||||
"""
|
||||
Returns True if the uid given exists as a link, and False otherwise.
|
||||
"""
|
||||
self.dbLock.acquire()
|
||||
returnValue = False
|
||||
if isinstance(uid, tuple) and self.database.edges.get(uid) is not None:
|
||||
returnValue = True
|
||||
self.dbLock.release()
|
||||
with self.dbLock:
|
||||
returnValue = isinstance(uid, tuple) and self.database.edges.get(uid) is not None
|
||||
return returnValue
|
||||
|
||||
def isLinkNoLock(self, uid: tuple) -> Union[bool, dict]:
|
||||
@@ -428,60 +388,43 @@ class EntitiesDB:
|
||||
return False
|
||||
|
||||
def getEntityType(self, uid: str) -> Union[None, dict]:
|
||||
self.dbLock.acquire()
|
||||
returnValue = None
|
||||
try:
|
||||
returnValue = self.getEntityNoLock(uid)['Entity Type']
|
||||
except KeyError:
|
||||
pass
|
||||
finally:
|
||||
self.dbLock.release()
|
||||
return returnValue
|
||||
with self.dbLock:
|
||||
returnValue = None
|
||||
try:
|
||||
returnValue = self.getEntityNoLock(uid)['Entity Type']
|
||||
except KeyError:
|
||||
pass
|
||||
finally:
|
||||
return returnValue
|
||||
|
||||
def mergeDatabases(self, newDB_nodes: dict, newDB_edges: dict, fromServer=True) -> None:
|
||||
"""
|
||||
Merges the existing database with the one provided.
|
||||
|
||||
Overwrites older attributes with newer ones.
|
||||
Overwrites older attributes with newer ones based on date last edited.
|
||||
"""
|
||||
self.dbLock.acquire()
|
||||
differenceGraph = nx.DiGraph()
|
||||
# Note: If we ever receive a node without a 'Date Last Edited' field, ignore it.
|
||||
differenceGraph.add_nodes_from([(x, newDB_nodes[x])
|
||||
for x in newDB_nodes if (x not in self.database.nodes()) or
|
||||
(
|
||||
x in self.database.nodes() and
|
||||
newDB_nodes[x].get('Date Last Edited') and
|
||||
newDB_nodes[x]['Date Last Edited'] >
|
||||
self.database.nodes[x]['Date Last Edited']
|
||||
)
|
||||
])
|
||||
differenceGraph.add_edges_from([(x, y, newDB_edges[(x, y)])
|
||||
for x, y in newDB_edges if ((x, y) not in self.database.edges()) or
|
||||
(
|
||||
(x, y) in self.database.edges() and
|
||||
newDB_edges[(x, y)].get('Date Last Edited') and
|
||||
newDB_edges[(x, y)]['Date Last Edited'] >
|
||||
self.database.edges[(x, y)]['Date Last Edited']
|
||||
)
|
||||
])
|
||||
if differenceGraph.number_of_nodes() > 0:
|
||||
self.database = nx.compose(self.database, differenceGraph)
|
||||
# Some nodes given by differenceGraph may be empty dicts, with an existing node's uid as the key.
|
||||
for node in differenceGraph.nodes:
|
||||
self.mainWindow.populateEntitiesWidget(self.database.nodes[node], add=True)
|
||||
|
||||
if not fromServer:
|
||||
if self.mainWindow.FCOM.isConnected():
|
||||
# diffNew = nx.DiGraph()
|
||||
# diffNew.add_nodes_from([(x, self.database.nodes[x])
|
||||
# for x in self.database.nodes() if x not in differenceGraph.nodes()
|
||||
# ])
|
||||
# diffNew.add_edges_from([(x, y, self.database.edges[(x, y)])
|
||||
# for x, y in self.database.edges() if (x, y) not in differenceGraph.edges()
|
||||
# ])
|
||||
with self.dbLock:
|
||||
differenceGraph = nx.DiGraph()
|
||||
differenceGraph.add_nodes_from([(n, nDict)
|
||||
for n, nDict in newDB_nodes.items() if (n not in self.database.nodes()) or
|
||||
(
|
||||
nDict.get('Date Last Edited', '') >
|
||||
self.database.nodes[n].get('Date Last Edited', '')
|
||||
)
|
||||
])
|
||||
differenceGraph.add_edges_from([(e[0], e[1], eDict)
|
||||
for e, eDict in newDB_edges.items() if (e not in self.database.edges()) or
|
||||
(
|
||||
eDict.get('Date Last Edited', '') >
|
||||
self.database.edges[e].get('Date Last Edited', '')
|
||||
)
|
||||
])
|
||||
if differenceGraph.number_of_nodes():
|
||||
self.database = nx.compose(self.database, differenceGraph)
|
||||
# Some nodes given by differenceGraph may be empty dicts, with an existing node's uid as the key.
|
||||
for node in differenceGraph.nodes:
|
||||
self.mainWindow.populateEntitiesWidget(self.database.nodes[node], add=True)
|
||||
|
||||
if not fromServer and self.mainWindow.FCOM.isConnected():
|
||||
# Assume we are already synced with server, so just send the difference.
|
||||
self.mainWindow.FCOM.syncDatabase(self.mainWindow.SETTINGS.value("Project/Server/Project"),
|
||||
differenceGraph)
|
||||
|
||||
self.dbLock.release()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
import contextlib
|
||||
from ast import literal_eval
|
||||
from typing import Union
|
||||
from pathlib import Path
|
||||
@@ -165,7 +166,7 @@ class CommunicationsHandler(QtCore.QObject):
|
||||
except ConnectionRefusedError:
|
||||
self.mainWindow.MESSAGEHANDLER.error("Did not connect: Server not running.", popUp=True, exc_info=False)
|
||||
except Exception as exception:
|
||||
self.mainWindow.MESSAGEHANDLER.error("Did not connect: " + str(exception))
|
||||
self.mainWindow.MESSAGEHANDLER.error(f"Did not connect: {str(exception)}")
|
||||
|
||||
try:
|
||||
if self.sock is not None:
|
||||
@@ -184,9 +185,7 @@ class CommunicationsHandler(QtCore.QObject):
|
||||
"""
|
||||
Check if socket is in a working state.
|
||||
"""
|
||||
if self.sock is not None and self.sock.fileno() != -1:
|
||||
return True
|
||||
return False
|
||||
return self.sock is not None and self.sock.fileno() != -1
|
||||
|
||||
def close(self) -> None:
|
||||
global closeSoftwareLock
|
||||
@@ -214,14 +213,13 @@ class CommunicationsHandler(QtCore.QObject):
|
||||
def decryptTransmission(self, bytesObject) -> Union[bytes, None]:
|
||||
decrypter = self.cipher.decryptor()
|
||||
try:
|
||||
message = decrypter.update(bytesObject) + decrypter.finalize()
|
||||
return message
|
||||
return decrypter.update(bytesObject) + decrypter.finalize()
|
||||
except InvalidTag:
|
||||
# If the ciphertext cannot be decrypted to a valid message, return None.
|
||||
return None
|
||||
|
||||
def transmitMessage(self, messageJson: dict, showErrorOnBrokenPipe: bool = True) -> None:
|
||||
self.mainWindow.MESSAGEHANDLER.debug('Sending Message: ' + str(messageJson))
|
||||
self.mainWindow.MESSAGEHANDLER.debug(f'Sending Message: {messageJson}')
|
||||
argEncoded = str(messageJson)
|
||||
largeMessageUUID = str(uuid4())
|
||||
try:
|
||||
@@ -244,19 +242,15 @@ class CommunicationsHandler(QtCore.QObject):
|
||||
"""
|
||||
try:
|
||||
self.transmitMessage({"Operation": "Close Socket", "Arguments": {}}, showErrorOnBrokenPipe=False)
|
||||
except OSError:
|
||||
# Typically this is due to bad file descriptor, i.e. server is closed.
|
||||
pass
|
||||
except AttributeError:
|
||||
# This happens if no connection was established while the software was running
|
||||
except (OSError, AttributeError):
|
||||
# OSError thrown due to bad file descriptor, i.e. server is closed.
|
||||
# AttributeError thrown if no connection was established while the software was running
|
||||
pass
|
||||
finally:
|
||||
try:
|
||||
with contextlib.suppress(OSError):
|
||||
# OSError thrown if the socket is already closed.
|
||||
if self.sock is not None:
|
||||
self.sock.shutdown(socket.SHUT_RDWR)
|
||||
except OSError:
|
||||
# This would typically occur if the socket is already closed.
|
||||
pass
|
||||
try:
|
||||
self.sock.close()
|
||||
finally:
|
||||
@@ -299,15 +293,12 @@ class CommunicationsHandler(QtCore.QObject):
|
||||
self.inbox.put(preInbox.pop(messageID).get("message"))
|
||||
|
||||
except socket.error as socketError:
|
||||
self.mainWindow.MESSAGEHANDLER.error('Socket Error: ' + str(socketError))
|
||||
self.mainWindow.MESSAGEHANDLER.error(f'Socket Error: {str(socketError)}')
|
||||
# If something happens, wait 2 seconds then try again.
|
||||
closeSoftwareLock.acquire()
|
||||
if not closeSoftware:
|
||||
closeSoftwareLock.release()
|
||||
time.sleep(2)
|
||||
else:
|
||||
closeSoftwareLock.release()
|
||||
break
|
||||
with closeSoftwareLock:
|
||||
if closeSoftware:
|
||||
break
|
||||
time.sleep(2)
|
||||
except ValueError:
|
||||
# E.g.: Unpack failed: incomplete input
|
||||
# In this case, we are being sent fragmented messages.
|
||||
@@ -333,14 +324,12 @@ class CommunicationsHandler(QtCore.QObject):
|
||||
continueTimestamp: int = 0) -> None:
|
||||
collector_entities_to_send = []
|
||||
for entity in collector_entities:
|
||||
try:
|
||||
with contextlib.suppress(KeyError):
|
||||
dereferenced_entity = dict(entity)
|
||||
collector_entities_to_send.append(dereferenced_entity)
|
||||
# Icon is not necessary for any collector as of now: 2022/4/3.
|
||||
# Cutting it out saves data.
|
||||
dereferenced_entity['Icon'] = ''
|
||||
except KeyError:
|
||||
pass
|
||||
message = {'Operation': 'Start Server Collector',
|
||||
'Arguments': {
|
||||
'collector_name': collector_name,
|
||||
@@ -372,14 +361,12 @@ class CommunicationsHandler(QtCore.QObject):
|
||||
resolution_uid: str) -> None:
|
||||
resolution_entities_to_send = []
|
||||
for entity in resolution_entities:
|
||||
try:
|
||||
with contextlib.suppress(KeyError):
|
||||
dereferenced_entity = dict(entity)
|
||||
resolution_entities_to_send.append(dereferenced_entity)
|
||||
# Icon is not necessary for any resolution as of now: 2022/1/2.
|
||||
# Cutting it out saves data.
|
||||
dereferenced_entity['Icon'] = ''
|
||||
except KeyError:
|
||||
pass
|
||||
message = {'Operation': 'Run Resolution',
|
||||
'Arguments': {
|
||||
'resolution_name': resolution_name,
|
||||
@@ -512,17 +499,13 @@ class CommunicationsHandler(QtCore.QObject):
|
||||
# Being verbose is better than prematurely optimizing for a few kbps of
|
||||
# network traffic.
|
||||
def receiveDatabaseUpdateEvent(self, entity_json: dict, add: int) -> None:
|
||||
try:
|
||||
with contextlib.suppress(KeyError):
|
||||
entity_json['Icon'] = QtCore.QByteArray(b64decode(entity_json['Icon']))
|
||||
except KeyError:
|
||||
pass
|
||||
self.receive_project_database_update.emit(entity_json, add)
|
||||
|
||||
def sendDatabaseUpdateEvent(self, project_name: str, entity_json: dict, add: int) -> None:
|
||||
try:
|
||||
with contextlib.suppress(KeyError):
|
||||
entity_json['Icon'] = entity_json['Icon'].toBase64().data()
|
||||
except KeyError:
|
||||
pass
|
||||
message = {"Operation": "Update Project Entities",
|
||||
"Arguments": {
|
||||
'project_name': project_name,
|
||||
@@ -573,30 +556,28 @@ class CommunicationsHandler(QtCore.QObject):
|
||||
"""
|
||||
if not filePath.exists() or not filePath.is_file():
|
||||
return
|
||||
fileHandler = open(filePath, 'rb')
|
||||
|
||||
currThread = threading.currentThread()
|
||||
while getattr(currThread, "continue_running", True):
|
||||
filePart = fileHandler.read(512)
|
||||
if not filePart:
|
||||
messageJson = {"Operation": "File Upload Done",
|
||||
with open(filePath, 'rb') as fileHandler:
|
||||
currThread = threading.currentThread()
|
||||
while getattr(currThread, "continue_running", True):
|
||||
filePart = fileHandler.read(512)
|
||||
if not filePart:
|
||||
messageJson = {"Operation": "File Upload Done",
|
||||
"Arguments": {
|
||||
'project_name': project_name,
|
||||
'file_name': file_name
|
||||
}}
|
||||
self.transmitMessage(messageJson)
|
||||
break
|
||||
messageJson = {"Operation": "File Upload",
|
||||
"Arguments": {
|
||||
'project_name': project_name,
|
||||
'file_name': file_name
|
||||
'file_name': file_name,
|
||||
'file_contents': filePart
|
||||
}}
|
||||
self.transmitMessage(messageJson)
|
||||
break
|
||||
messageJson = {"Operation": "File Upload",
|
||||
"Arguments": {
|
||||
'project_name': project_name,
|
||||
'file_name': file_name,
|
||||
'file_contents': filePart
|
||||
}}
|
||||
self.transmitMessage(messageJson)
|
||||
fileHandler.close()
|
||||
|
||||
def sendFileAbort(self, project_name: str, file_name: str) -> None:
|
||||
try:
|
||||
with contextlib.suppress(KeyError):
|
||||
uploadToAbort = self.uploadingFiles.pop(file_name)
|
||||
uploadToAbort.continue_running = False
|
||||
messageJson = {"Operation": "File Upload Abort",
|
||||
@@ -605,8 +586,6 @@ class CommunicationsHandler(QtCore.QObject):
|
||||
'file_name': file_name
|
||||
}}
|
||||
self.transmitMessage(messageJson)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def scanInbox(self) -> None:
|
||||
"""
|
||||
@@ -619,15 +598,14 @@ class CommunicationsHandler(QtCore.QObject):
|
||||
message = self.inbox.get(timeout=0.2)
|
||||
except Empty:
|
||||
with closeSoftwareLock:
|
||||
if not closeSoftware:
|
||||
time.sleep(0.2)
|
||||
continue
|
||||
else:
|
||||
if closeSoftware:
|
||||
return
|
||||
time.sleep(0.2)
|
||||
continue
|
||||
if prevMesg == message:
|
||||
# Same message, do not waste time handling.
|
||||
continue
|
||||
self.mainWindow.MESSAGEHANDLER.debug('Message to handle: ' + str(message))
|
||||
self.mainWindow.MESSAGEHANDLER.debug(f'Message to handle: {str(message)}')
|
||||
operation = message['Operation']
|
||||
arguments = message['Arguments']
|
||||
if operation == 'Get Server Resolutions':
|
||||
@@ -667,8 +645,8 @@ class CommunicationsHandler(QtCore.QObject):
|
||||
elif operation == "Start Collector":
|
||||
self.receiveStartCollector(**arguments)
|
||||
else:
|
||||
self.mainWindow.MESSAGEHANDLER.warning('Unhandled message: ' + str(message) +
|
||||
' On Operation: ' + str(operation))
|
||||
self.mainWindow.MESSAGEHANDLER.warning(f'Unhandled message: {str(message)} On Operation: '
|
||||
f'{str(operation)}')
|
||||
prevMesg = message
|
||||
|
||||
def handleStatusMessage(self, operation: str, message: str, status_code: int) -> None:
|
||||
@@ -686,61 +664,53 @@ class CommunicationsHandler(QtCore.QObject):
|
||||
:param status_code:
|
||||
:return:
|
||||
"""
|
||||
if status_code != 200:
|
||||
if status_code == 404 and message == 'No project with the specified name exists!':
|
||||
self.close_project_signal.emit()
|
||||
else:
|
||||
self.status_message_signal.emit('Operation ' + operation + ' failed with status code ' +
|
||||
str(status_code) + ': ' + message, True)
|
||||
else:
|
||||
if operation == 'Create Project':
|
||||
# No need to do anything here. Creating a new project also opens it.
|
||||
pass
|
||||
elif operation == 'Open Project':
|
||||
projectName = message.split(': ', 1)[1]
|
||||
self.open_project_signal.emit(projectName)
|
||||
# Show the user that the server is doing something.
|
||||
elif operation == 'Opening Project':
|
||||
self.status_message_signal.emit(message, True)
|
||||
elif operation == 'Close Project':
|
||||
self.close_project_signal.emit()
|
||||
elif operation == 'Create Canvas':
|
||||
# No need to do anything here. Creating a new canvas also opens it.
|
||||
pass
|
||||
elif operation == 'Open Canvas':
|
||||
canvas_name = message.split(': ', 1)[1]
|
||||
self.open_project_canvas_signal.emit(canvas_name)
|
||||
elif operation == 'Close Canvas':
|
||||
canvas_name = message.split(': ', 1)[1]
|
||||
self.close_project_canvas_signal.emit(canvas_name)
|
||||
elif operation == 'Connect To Server':
|
||||
server_name = message.split(': ', 1)[1]
|
||||
self.connected_to_server_listener.emit(server_name)
|
||||
elif operation == 'File Upload':
|
||||
file_name = message.split(': ', 1)[1]
|
||||
self.file_upload_finished_signal.emit(file_name)
|
||||
elif operation == 'File Download Done':
|
||||
file_name = message.split(': ', 1)[1]
|
||||
self.receiveFileDoneListener(file_name)
|
||||
elif operation == 'Abort Resolution':
|
||||
if status_code == 200:
|
||||
if operation == 'Abort Resolution':
|
||||
# Remove resolution from resolutions list.
|
||||
resolution_uid = message.split(': ', 1)[1]
|
||||
self.remove_server_resolution_from_running_signal.emit(resolution_uid)
|
||||
elif operation == 'Close Canvas':
|
||||
canvas_name = message.split(': ', 1)[1]
|
||||
self.close_project_canvas_signal.emit(canvas_name)
|
||||
elif operation == 'Close Project':
|
||||
self.close_project_signal.emit()
|
||||
elif operation == 'Connect To Server':
|
||||
server_name = message.split(': ', 1)[1]
|
||||
self.connected_to_server_listener.emit(server_name)
|
||||
elif operation in {'Create Project', 'Create Canvas', 'Stop Collector'}:
|
||||
# No need to do anything for these.
|
||||
pass
|
||||
elif operation == 'Delete Project':
|
||||
# Remove project from server projects list.
|
||||
project_name = message.split(': ', 1)[1]
|
||||
self.delete_server_project_signal.emit(project_name)
|
||||
elif operation == 'File Download Done':
|
||||
file_name = message.split(': ', 1)[1]
|
||||
self.receiveFileDoneListener(file_name)
|
||||
elif operation == 'File Upload Abort':
|
||||
file_name = message.split(': ', 1)[1]
|
||||
# Remove file from uploading files list.
|
||||
self.file_upload_abort_signal.emit(file_name)
|
||||
elif operation == 'Stop Collector':
|
||||
# No need to do anything here - stopping collectors is only done by the client.
|
||||
pass
|
||||
elif operation == 'File Upload':
|
||||
file_name = message.split(': ', 1)[1]
|
||||
self.file_upload_finished_signal.emit(file_name)
|
||||
elif operation == 'Open Canvas':
|
||||
canvas_name = message.split(': ', 1)[1]
|
||||
self.open_project_canvas_signal.emit(canvas_name)
|
||||
elif operation == 'Open Project':
|
||||
projectName = message.split(': ', 1)[1]
|
||||
self.open_project_signal.emit(projectName)
|
||||
elif operation == 'Opening Project':
|
||||
self.status_message_signal.emit(message, True)
|
||||
else:
|
||||
self.mainWindow.MESSAGEHANDLER.warning('Unhandled status message: ' + message +
|
||||
' Code: ' + str(status_code) +
|
||||
' On Operation: ' + str(operation))
|
||||
self.mainWindow.MESSAGEHANDLER.warning(f'Unhandled status message: {message} Code: {status_code} '
|
||||
f'On Operation: {operation}')
|
||||
|
||||
elif status_code == 404 and message == 'No project with the specified name exists!':
|
||||
self.close_project_signal.emit()
|
||||
else:
|
||||
self.status_message_signal.emit(f'Operation {operation} failed with status code {status_code}: {message}',
|
||||
True)
|
||||
|
||||
def receiveFile(self, project_name: str, file_name: str, saveDir: Path) -> None:
|
||||
# Do not download files already being downloaded.
|
||||
@@ -761,18 +731,19 @@ class CommunicationsHandler(QtCore.QObject):
|
||||
fileHandler.write(file_contents)
|
||||
except Exception:
|
||||
# In case something goes wrong in the middle of writing.
|
||||
self.mainWindow.MESSAGEHANDLER.warning('Received data for file: ' + file_name +
|
||||
' but no valid file handler exists for this file.')
|
||||
self.mainWindow.MESSAGEHANDLER.warning(f'Received data for file: {file_name} but no valid file handler '
|
||||
f'exists for this file.')
|
||||
|
||||
def receiveFileDoneListener(self, file_name: str) -> None:
|
||||
fileHandler = self.downloadingFiles.pop(file_name)
|
||||
if fileHandler is None:
|
||||
self.mainWindow.MESSAGEHANDLER.warning('Received file: ' + file_name +
|
||||
' but no file handler exists for this file.')
|
||||
self.mainWindow.MESSAGEHANDLER.warning(f'Received file: {file_name} but no file handler exists for this '
|
||||
f'file.')
|
||||
|
||||
return
|
||||
|
||||
fileHandler.close()
|
||||
self.status_message_signal.emit('Finished downloading file from server: ' + file_name, True)
|
||||
self.status_message_signal.emit(f'Finished downloading file from server: {file_name}', True)
|
||||
|
||||
def receiveFileAbort(self, project_name: str, file_name: str) -> None:
|
||||
messageJson = {"Operation": "File Download Abort",
|
||||
@@ -813,12 +784,10 @@ class CommunicationsHandler(QtCore.QObject):
|
||||
:param file_name:
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
with contextlib.suppress(KeyError):
|
||||
uploadToAbort = self.uploadingFiles.pop(file_name)
|
||||
uploadToAbort.continue_running = False
|
||||
self.file_upload_abort_signal.emit(file_name)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def sendFileAbortAll(self, project_name: str) -> None:
|
||||
"""
|
||||
@@ -828,7 +797,7 @@ class CommunicationsHandler(QtCore.QObject):
|
||||
:return:
|
||||
"""
|
||||
for file_name in dict(self.uploadingFiles):
|
||||
try:
|
||||
with contextlib.suppress(KeyError):
|
||||
uploadToAbort = self.uploadingFiles.pop(file_name)
|
||||
uploadToAbort.continue_running = False
|
||||
messageJson = {"Operation": "File Upload Abort",
|
||||
@@ -837,8 +806,6 @@ class CommunicationsHandler(QtCore.QObject):
|
||||
'file_name': file_name
|
||||
}}
|
||||
self.transmitMessage(messageJson)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def receiveFileAbortAll(self, project_name: str) -> None:
|
||||
"""
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import contextlib
|
||||
import re
|
||||
import json
|
||||
import sys
|
||||
@@ -71,8 +72,8 @@ class WorkspaceWidget(QtWidgets.QWidget):
|
||||
self.docPane.deleteLater()
|
||||
|
||||
def setDocTitleAndContents(self, title: Union[str, None] = None, content: Union[str, None] = None) -> None:
|
||||
self.docPaneTitleText = title if title else "Document Name"
|
||||
self.docPaneBodyText = content if content else "Document Summary"
|
||||
self.docPaneTitleText = title or "Document Name"
|
||||
self.docPaneBodyText = content or "Document Summary"
|
||||
if self.docAndCanvasLayout.count() == 2:
|
||||
self.docPane.documentTitleWidget.setText(title)
|
||||
self.docPane.documentSummaryWidget.setPlainText(content)
|
||||
@@ -89,38 +90,33 @@ class TabBar(QtWidgets.QTabBar):
|
||||
self.setMovable(True)
|
||||
|
||||
def mouseDoubleClickEvent(self, event) -> None:
|
||||
if event.button() == QtGui.Qt.LeftButton:
|
||||
currIndex = self.currentIndex()
|
||||
currName = self.tabText(currIndex)
|
||||
currView = self.parent().getViewAtIndex(currIndex)
|
||||
if event.button() != QtGui.Qt.LeftButton:
|
||||
return
|
||||
currIndex = self.currentIndex()
|
||||
currName = self.tabText(currIndex)
|
||||
currView = self.parent().getViewAtIndex(currIndex)
|
||||
|
||||
renameOrDeleteDialog = RenameOrDeleteTabDialog(currView.synced, currName)
|
||||
dialogResult = renameOrDeleteDialog.exec()
|
||||
renameOrDeleteDialog = RenameOrDeleteTabDialog(currView.synced, currName)
|
||||
if not renameOrDeleteDialog.exec():
|
||||
return
|
||||
|
||||
if dialogResult:
|
||||
newTabName = renameOrDeleteDialog.newNameTextBox.text()
|
||||
delete = renameOrDeleteDialog.deleteCheckbox.isChecked()
|
||||
else:
|
||||
return
|
||||
newTabName = renameOrDeleteDialog.newNameTextBox.text()
|
||||
if renameOrDeleteDialog.deleteCheckbox.isChecked():
|
||||
sceneToClose = self.parent().canvasTabs[currName].scene()
|
||||
groupNodes = [item for item in sceneToClose.items() if isinstance(item, Entity.GroupNode)]
|
||||
for groupNode in groupNodes:
|
||||
sceneToClose.removeNode(groupNode)
|
||||
self.parent().closeTab(currName)
|
||||
return
|
||||
|
||||
if delete:
|
||||
sceneToClose = self.parent().canvasTabs[currName].scene()
|
||||
groupNodes = [item for item in sceneToClose.items() if isinstance(item, Entity.GroupNode)]
|
||||
for groupNode in groupNodes:
|
||||
sceneToClose.removeNode(groupNode)
|
||||
self.parent().closeTab(currName)
|
||||
return
|
||||
|
||||
if newTabName == currName or newTabName == "":
|
||||
return
|
||||
if self.parent().isCanvasNameAvailable(newTabName):
|
||||
self.parent().renameCanvas(currName, newTabName)
|
||||
self.setTabText(currIndex, newTabName)
|
||||
currView.name = newTabName
|
||||
else:
|
||||
self.messageHandler.info(
|
||||
"Failed renaming tab: Canvas name already exists.",
|
||||
popUp=True)
|
||||
if newTabName in [currName, ""]:
|
||||
return
|
||||
if self.parent().isCanvasNameAvailable(newTabName):
|
||||
self.parent().renameCanvas(currName, newTabName)
|
||||
self.setTabText(currIndex, newTabName)
|
||||
currView.name = newTabName
|
||||
else:
|
||||
self.messageHandler.info("Failed renaming tab: Canvas name already exists.", popUp=True)
|
||||
|
||||
|
||||
class RenameOrDeleteTabDialog(QtWidgets.QDialog):
|
||||
@@ -241,11 +237,8 @@ class TabbedPane(QtWidgets.QTabWidget):
|
||||
|
||||
def unmarkSyncedCanvasesByName(self, canvasToUnSync: str = None) -> None:
|
||||
if canvasToUnSync is not None:
|
||||
try:
|
||||
with contextlib.suppress(ValueError):
|
||||
self.syncedTabs.remove(canvasToUnSync)
|
||||
except ValueError:
|
||||
# In case the tab to unSync isn't actually synced.
|
||||
pass
|
||||
syncIndex = self.getTabIndexByName(canvasToUnSync)
|
||||
syncView = self.getViewAtIndex(syncIndex)
|
||||
self.setTabIcon(syncIndex, QtGui.QIcon())
|
||||
@@ -262,9 +255,7 @@ class TabbedPane(QtWidgets.QTabWidget):
|
||||
"""
|
||||
Checks if the specified canvas name is available.
|
||||
"""
|
||||
if canvasName in self.canvasTabs:
|
||||
return False
|
||||
return True
|
||||
return canvasName not in self.canvasTabs
|
||||
|
||||
def renameCanvas(self, currName: str, newName: str) -> None:
|
||||
self.canvasTabs[newName] = self.canvasTabs.pop(currName)
|
||||
@@ -294,10 +285,8 @@ class TabbedPane(QtWidgets.QTabWidget):
|
||||
self.removeTab(tabIndex)
|
||||
break
|
||||
self.canvasTabs.pop(tabName)
|
||||
try:
|
||||
with contextlib.suppress(KeyError):
|
||||
self.tabsNotesDict.pop(tabName)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def hideTab(self, index) -> None:
|
||||
self.removeTab(index)
|
||||
@@ -311,18 +300,11 @@ class TabbedPane(QtWidgets.QTabWidget):
|
||||
self.setTabIcon(syncIndex, QtGui.QIcon(self.resourceHandler.getIcon("uploading")))
|
||||
|
||||
def getTabIndexByName(self, tabName):
|
||||
count = 0
|
||||
for tab in self.canvasTabs:
|
||||
if tab == tabName:
|
||||
return count
|
||||
count += 1
|
||||
return None
|
||||
return next((count for count, tab in enumerate(self.canvasTabs) if tab == tabName), None)
|
||||
|
||||
def getSceneByName(self, tabName):
|
||||
tabView = self.canvasTabs.get(tabName)
|
||||
if tabView is not None:
|
||||
return tabView.scene()
|
||||
return None
|
||||
return tabView.scene() if tabView is not None else None
|
||||
|
||||
def getNameOfScene(self, scene) -> str:
|
||||
for tab in self.canvasTabs:
|
||||
@@ -367,17 +349,16 @@ class TabbedPane(QtWidgets.QTabWidget):
|
||||
|
||||
# Having a very granular progress bar results in a massive slowdown (i.e. resolutions take 5x< the time).
|
||||
steps = 3
|
||||
progress = QtWidgets.QProgressDialog('Resolving new nodes for resolution: ' + resolution_name +
|
||||
', please wait...', 'Abort Resolving Nodes', 0, steps, self)
|
||||
progress = QtWidgets.QProgressDialog(f'Resolving new nodes for resolution: {resolution_name}, please wait...',
|
||||
'Abort Resolving Nodes', 0, steps, self)
|
||||
|
||||
progress.setWindowModality(QtCore.Qt.WindowModal)
|
||||
progress.setMinimumDuration(1500)
|
||||
|
||||
# Get all the entities, then split it into several lists, to make searching & iterating through them faster.
|
||||
allEntities = [(entity['uid'], (entity[list(entity)[1]], entity['Entity Type']))
|
||||
for entity in self.entityDB.getAllEntities()]
|
||||
# In case we have no entities in the database when the resolution finishes, i.e. the user deletes the origin
|
||||
# node for the resolution, or runs something that creates nodes from nothing.
|
||||
if allEntities:
|
||||
if allEntities := [(entity['uid'], (entity[list(entity)[1]], entity['Entity Type']))
|
||||
for entity in self.entityDB.getAllEntities()]:
|
||||
allEntityUIDs, allEntityPrimaryFieldsAndTypes = map(list, zip(*allEntities))
|
||||
else:
|
||||
allEntityUIDs = []
|
||||
@@ -412,7 +393,7 @@ class TabbedPane(QtWidgets.QTabWidget):
|
||||
try:
|
||||
notesField = newNodeJSON.pop('Notes')
|
||||
if existingEntityJSON.get('Notes'):
|
||||
existingEntityJSON['Notes'] += '\n' + notesField
|
||||
existingEntityJSON['Notes'] += f"\n{notesField}"
|
||||
else:
|
||||
existingEntityJSON['Notes'] = str(notesField)
|
||||
except KeyError:
|
||||
@@ -441,9 +422,8 @@ class TabbedPane(QtWidgets.QTabWidget):
|
||||
nodesCreatedCount += 1
|
||||
|
||||
progress.setValue(1)
|
||||
for resultListIndex in range(len(newNodeUIDs)):
|
||||
outputEntityUID = newNodeUIDs[resultListIndex]
|
||||
parentsDict = resolution_result[resultListIndex][1]
|
||||
for outputEntityUID, resolutionResultElement in zip(newNodeUIDs, resolution_result):
|
||||
parentsDict = resolutionResultElement[1]
|
||||
for parentID in parentsDict:
|
||||
parentUID = parentID
|
||||
if isinstance(parentUID, int):
|
||||
@@ -458,7 +438,7 @@ class TabbedPane(QtWidgets.QTabWidget):
|
||||
if newLinkUID in allLinks:
|
||||
linkJson = self.entityDB.getLinkIfExists(newLinkUID)
|
||||
if resolutionName not in linkJson['Notes']:
|
||||
linkJson['Notes'] += '\nConnection also produced by Resolution: ' + resolutionName
|
||||
linkJson['Notes'] += f"\nConnection also produced by Resolution: {resolutionName}"
|
||||
self.entityDB.addLink(linkJson, fromServer=True)
|
||||
linksUpdatedCount += 1
|
||||
else:
|
||||
@@ -477,10 +457,11 @@ class TabbedPane(QtWidgets.QTabWidget):
|
||||
progress.setValue(3)
|
||||
self.entityDB.resetTimeline()
|
||||
self.mainWindow.saveProject()
|
||||
self.mainWindow.MESSAGEHANDLER.info('Resolution ' + resolution_name + ' completed successfully: ' +
|
||||
str(nodesCreatedCount) + ' new nodes created, ' + str(nodesUpdatedCount) +
|
||||
' existing nodes updated. New links created: ' + str(linksCreatedCount) +
|
||||
', links updated: ' + str(linksUpdatedCount))
|
||||
self.mainWindow.MESSAGEHANDLER.info(f'Resolution {resolution_name} completed successfully: '
|
||||
f'{str(nodesCreatedCount)} new nodes created, {str(nodesUpdatedCount)} '
|
||||
f'existing nodes updated. New links created: {str(linksCreatedCount)}, '
|
||||
f'links updated: {str(linksUpdatedCount)}')
|
||||
|
||||
return newNodeUIDs
|
||||
|
||||
def linkAddHelper(self, links) -> None:
|
||||
@@ -536,10 +517,10 @@ class TabbedPane(QtWidgets.QTabWidget):
|
||||
else:
|
||||
scene.addLinkProgrammatic((newLink[0], newLink[1]), newLink[2])
|
||||
|
||||
elif parentUID in scene.nodesDict and uid in scene.nodesDict:
|
||||
elif parentUID in scene.nodesDict:
|
||||
# Need to send this to server, since it won't be drawn otherwise.
|
||||
scene.addLinkDragDrop(scene.nodesDict[parentUID], scene.nodesDict[uid], newLink[2])
|
||||
elif parentUID not in scene.nodesDict and uid in scene.nodesDict:
|
||||
elif uid in scene.nodesDict:
|
||||
if parentUID not in scene.sceneGraph.nodes:
|
||||
nodeJSON = self.entityDB.getEntity(parentUID)
|
||||
|
||||
@@ -614,16 +595,14 @@ class TabbedPane(QtWidgets.QTabWidget):
|
||||
|
||||
canvasDBPath = self.getCanvasDBPath()
|
||||
tabsNotesPath = self.getTabsNotesPath()
|
||||
canvasDBPathTmp = canvasDBPath.with_suffix(canvasDBPath.suffix + '.tmp')
|
||||
canvasNotesPathTmp = tabsNotesPath.with_suffix(tabsNotesPath.suffix + '.tmp')
|
||||
canvasDBPathTmp = canvasDBPath.with_suffix(f'{canvasDBPath.suffix}.tmp')
|
||||
canvasNotesPathTmp = tabsNotesPath.with_suffix(f'{tabsNotesPath.suffix}.tmp')
|
||||
|
||||
# Save canvases
|
||||
with open(canvasDBPathTmp, "wb") as canvasDBFile:
|
||||
saveJson = {}
|
||||
for canvasName in self.canvasTabs:
|
||||
saveJson[canvasName] = [
|
||||
self.resourceHandler.deconstructGraphForFileDump(self.canvasTabs[canvasName].scene().sceneGraph),
|
||||
self.canvasTabs[canvasName].scene().scenePos]
|
||||
saveJson = {canvasName: [self.resourceHandler.deconstructGraphForFileDump(self.canvasTabs[canvasName].scene().sceneGraph),
|
||||
self.canvasTabs[canvasName].scene().scenePos] for canvasName in self.canvasTabs}
|
||||
|
||||
dump(saveJson, canvasDBFile)
|
||||
move(canvasDBPathTmp, canvasDBPath)
|
||||
|
||||
@@ -665,8 +644,8 @@ class TabbedPane(QtWidgets.QTabWidget):
|
||||
self.resourceHandler.reconstructGraphFullFromFile(savedJson[canvasName][0]),
|
||||
savedJson[canvasName][1])
|
||||
except Exception as exc:
|
||||
self.messageHandler.error("Exception occurred when opening tabs: " + str(exc) +
|
||||
"\nSkipping opening tabs.", popUp=True)
|
||||
self.messageHandler.error(f"Exception occurred when opening tabs: {str(exc)}\nSkipping opening tabs.",
|
||||
popUp=True)
|
||||
|
||||
def currentTabChangedListener(self, newIndex: int) -> None:
|
||||
if self.previousTab in self.canvasTabs:
|
||||
@@ -882,8 +861,7 @@ class CanvasView(QtWidgets.QGraphicsView):
|
||||
self.centerOn(node)
|
||||
|
||||
def dragMoveEvent(self, event) -> None:
|
||||
itemsMoved = self.scene().selectedItems()
|
||||
if len(itemsMoved) != 0:
|
||||
if itemsMoved := self.scene().selectedItems():
|
||||
self.ensureVisible(itemsMoved[0], 50, 50)
|
||||
|
||||
def dragEnterEvent(self, event) -> None:
|
||||
@@ -934,26 +912,11 @@ class CanvasView(QtWidgets.QGraphicsView):
|
||||
entityJson = self.tabbedPane.entityDB.addEntity(nodeJson)
|
||||
entityUID = entityJson['uid']
|
||||
|
||||
if entityUID not in self.scene().sceneGraph.nodes():
|
||||
if entityJson['Entity Type'] == 'EntityGroup':
|
||||
newGroup = self.tabbedPane.mainWindow.copyGroupEntity(entityUID, self.scene())
|
||||
if newGroup is not None:
|
||||
newNode = self.scene().addNodeProgrammatic(newGroup['uid'], newGroup['Child UIDs'])
|
||||
newNode.setPos(pos.x() - 20, pos.y() - 20)
|
||||
else:
|
||||
self.tabbedPane.messageHandler.warning("Cannot add selected Group Node to scene: Scene "
|
||||
"already contains all nodes that the Group Node "
|
||||
"currently contains.", popUp=True)
|
||||
else:
|
||||
self.scene().addNodeDragDrop(
|
||||
entityUID,
|
||||
pos.x() - 20,
|
||||
pos.y() - 20)
|
||||
else:
|
||||
if entityUID in self.scene().sceneGraph.nodes():
|
||||
wasGrouped = False
|
||||
for groupNode in [node for node in self.items() if isinstance(node, Entity.GroupNode)]:
|
||||
wasGrouped = \
|
||||
groupNode.removeSpecificItemFromGroupIfExists(entityUID)
|
||||
groupNode.removeSpecificItemFromGroupIfExists(entityUID)
|
||||
if wasGrouped:
|
||||
self.removeGroupNodeLinksForUID(groupNode.uid, entityUID)
|
||||
groupNodeJson = self.tabbedPane.entityDB.getEntity(groupNode.uid)
|
||||
@@ -976,6 +939,20 @@ class CanvasView(QtWidgets.QGraphicsView):
|
||||
pos.y() - 20
|
||||
)
|
||||
break
|
||||
elif entityJson['Entity Type'] == 'EntityGroup':
|
||||
newGroup = self.tabbedPane.mainWindow.copyGroupEntity(entityUID, self.scene())
|
||||
if newGroup is not None:
|
||||
newNode = self.scene().addNodeProgrammatic(newGroup['uid'], newGroup['Child UIDs'])
|
||||
newNode.setPos(pos.x() - 20, pos.y() - 20)
|
||||
else:
|
||||
self.tabbedPane.messageHandler.warning("Cannot add selected Group Node to scene: Scene "
|
||||
"already contains all nodes that the Group Node "
|
||||
"currently contains.", popUp=True)
|
||||
else:
|
||||
self.scene().addNodeDragDrop(
|
||||
entityUID,
|
||||
pos.x() - 20,
|
||||
pos.y() - 20)
|
||||
if len(nodeJson) > 1:
|
||||
primaryField = self.tabbedPane.resourceHandler.getPrimaryFieldForEntityType(
|
||||
entityJson['Entity Type'])
|
||||
@@ -1001,9 +978,7 @@ class CanvasView(QtWidgets.QGraphicsView):
|
||||
# Entities are unique - only one instance exists in each canvas.
|
||||
# This should search all group nodes, regardless of whether they are nested or not.
|
||||
for groupNode in [node for node in self.items() if isinstance(node, Entity.GroupNode)]:
|
||||
wasGrouped = \
|
||||
groupNode.removeSpecificItemFromGroupIfExists(entityUID)
|
||||
if wasGrouped:
|
||||
if groupNode.removeSpecificItemFromGroupIfExists(entityUID):
|
||||
self.removeGroupNodeLinksForUID(groupNode.uid, entityUID)
|
||||
|
||||
# Should not be needed.
|
||||
@@ -1014,11 +989,9 @@ class CanvasView(QtWidgets.QGraphicsView):
|
||||
self.tabbedPane.entityDB.addEntity(groupNodeJson)
|
||||
|
||||
break
|
||||
try:
|
||||
with contextlib.suppress(nx.exception.NetworkXError):
|
||||
# Exception thrown if node was already removed.
|
||||
self.scene().sceneGraph.remove_node(entityUID)
|
||||
except nx.exception.NetworkXError:
|
||||
# Node is already removed
|
||||
pass
|
||||
|
||||
def removeGroupNodeLinksForUID(self, groupUID, nodeUID) -> None:
|
||||
self.scene().removeGroupNodeLinksForUID(groupUID, nodeUID)
|
||||
@@ -1035,19 +1008,20 @@ class CanvasView(QtWidgets.QGraphicsView):
|
||||
self.adjustSceneRect()
|
||||
|
||||
def wheelEvent(self, event) -> None:
|
||||
if len(self.scene().items()) > 0:
|
||||
if event.angleDelta().y() > 0:
|
||||
if self.zoom == 0:
|
||||
return
|
||||
factor = 1.25
|
||||
self.zoom += 1
|
||||
else:
|
||||
# Prevent user from zooming out indefinitely.
|
||||
if self.zoom < -11:
|
||||
return
|
||||
factor = 0.8
|
||||
self.zoom -= 1
|
||||
self.scale(factor, factor)
|
||||
if len(self.scene().items()) == 0:
|
||||
return
|
||||
if event.angleDelta().y() > 0:
|
||||
if self.zoom == 0:
|
||||
return
|
||||
factor = 1.25
|
||||
self.zoom += 1
|
||||
else:
|
||||
# Prevent user from zooming out indefinitely.
|
||||
if self.zoom < -11:
|
||||
return
|
||||
factor = 0.8
|
||||
self.zoom -= 1
|
||||
self.scale(factor, factor)
|
||||
|
||||
def mousePressEvent(self, event) -> None:
|
||||
if event.button() == QtCore.Qt.MouseButton.RightButton and \
|
||||
@@ -1058,7 +1032,7 @@ class CanvasView(QtWidgets.QGraphicsView):
|
||||
items = self.scene().selectedItems()
|
||||
groupItems = [groupItem for groupItem in items if isinstance(groupItem, Entity.GroupNode)]
|
||||
linkItems = [linkItem for linkItem in items if isinstance(linkItem, Entity.BaseConnector)]
|
||||
if len(groupItems) > 0:
|
||||
if groupItems:
|
||||
self.actionUngroup.setDisabled(False)
|
||||
self.actionUngroup.setEnabled(True)
|
||||
else:
|
||||
@@ -1070,7 +1044,7 @@ class CanvasView(QtWidgets.QGraphicsView):
|
||||
else:
|
||||
self.actionGroup.setDisabled(True)
|
||||
self.actionGroup.setEnabled(False)
|
||||
if len(linkItems) > 0:
|
||||
if linkItems:
|
||||
self.actionLinkDelete.setDisabled(False)
|
||||
self.actionLinkDelete.setEnabled(True)
|
||||
else:
|
||||
@@ -1112,8 +1086,7 @@ class CanvasView(QtWidgets.QGraphicsView):
|
||||
prompt = SendToOtherTabCanvasSelector(otherTabs)
|
||||
|
||||
if prompt.exec():
|
||||
otherCanvasName = prompt.canvasNameSelector.currentText()
|
||||
if otherCanvasName:
|
||||
if otherCanvasName := prompt.canvasNameSelector.currentText():
|
||||
otherCanvas = self.tabbedPane.canvasTabs[otherCanvasName].scene()
|
||||
for entityToSend in entitiesToSend:
|
||||
if otherCanvas.sceneGraph.nodes.get(entityToSend) is None:
|
||||
@@ -1152,8 +1125,7 @@ class CanvasView(QtWidgets.QGraphicsView):
|
||||
if link[1] not in self.scene().sceneGraph.nodes])
|
||||
for groupEntity in [linkedGroupEntity for linkedGroupEntity in linkedEntities
|
||||
if linkedGroupEntity.endswith('@')]:
|
||||
newEntityJSON = self.tabbedPane.mainWindow.copyGroupEntity(groupEntity, self.scene())
|
||||
if newEntityJSON:
|
||||
if newEntityJSON := self.tabbedPane.mainWindow.copyGroupEntity(groupEntity, self.scene()):
|
||||
newNode = self.scene().addNodeProgrammatic(newEntityJSON['uid'], newEntityJSON['Child UIDs'])
|
||||
linkedEntities.remove(groupEntity)
|
||||
newNode.setSelected(True)
|
||||
@@ -1168,7 +1140,7 @@ class CanvasView(QtWidgets.QGraphicsView):
|
||||
# Ref: https://qtcentre.org/threads/10975-Help-Export-QGraphicsView-to-Image-File
|
||||
# Rendering best optimized to rgb32 and argb32_premultiplied.
|
||||
# Ref: https://doc.qt.io/qtforpython/PySide6/QtGui/QImage.html?highlight=qimage#image-formats
|
||||
selectedItems = [item for item in self.scene().selectedItems()]
|
||||
selectedItems = list(self.scene().selectedItems())
|
||||
for item in selectedItems:
|
||||
item.setSelected(False)
|
||||
if justViewport:
|
||||
@@ -1275,14 +1247,13 @@ class CanvasScene(QtWidgets.QGraphicsScene):
|
||||
self.nodesDict[item.uid] = item
|
||||
self.addItem(item)
|
||||
item.setPos(QtCore.QPointF(x, y))
|
||||
self.parent().mainWindow.MESSAGEHANDLER.info('Added node: ' + str(item.uid) + ' | ' +
|
||||
item.labelItem.toPlainText())
|
||||
self.parent().mainWindow.MESSAGEHANDLER.info(f'Added node: {str(item.uid)} | {item.labelItem.toPlainText()}')
|
||||
|
||||
def addLinkToScene(self, link: Entity.BaseConnector) -> None:
|
||||
self.linksDict[link.startItem().uid + link.endItem().uid] = link
|
||||
self.addItem(link)
|
||||
self.parent().mainWindow.MESSAGEHANDLER.info('Added link: (' + link.startItem().uid + ", " +
|
||||
link.endItem().uid + ') | ' + link.labelItem.text())
|
||||
self.parent().mainWindow.MESSAGEHANDLER.info(f'Added link: ({link.startItem().uid}, {link.endItem().uid}) | '
|
||||
f'{link.labelItem.text()}')
|
||||
|
||||
def appendSelectedItemsToGroupToggle(self) -> None:
|
||||
if self.linking:
|
||||
@@ -1311,22 +1282,22 @@ class CanvasScene(QtWidgets.QGraphicsScene):
|
||||
self.parent().mainWindow.setStatus('Too many items selected to link, please only choose a total of 2.')
|
||||
elif len(self.itemsToLink) == 2:
|
||||
potentialUID = (self.itemsToLink[0].uid, self.itemsToLink[1].uid)
|
||||
if not self.parent().entityDB.isLink(potentialUID):
|
||||
if self.parent().entityDB.addLink({"uid": potentialUID}) is not None:
|
||||
self.addLinkDragDrop(self.itemsToLink[0],
|
||||
self.itemsToLink[1])
|
||||
self.editLinkProperties(potentialUID)
|
||||
if not self.parent().entityDB.isLink(potentialUID) and \
|
||||
self.parent().entityDB.addLink({"uid": potentialUID}) is not None:
|
||||
self.addLinkDragDrop(self.itemsToLink[0], self.itemsToLink[1])
|
||||
self.editLinkProperties(potentialUID)
|
||||
self.parent().mainWindow.toggleLinkingMode()
|
||||
|
||||
elif self.appendingToGroup:
|
||||
selectedGroupItems = [item for item in self.selectedItems() if isinstance(item, Entity.GroupNode)]
|
||||
if len(selectedGroupItems) < 1:
|
||||
if not selectedGroupItems:
|
||||
pass
|
||||
elif len(selectedGroupItems) > 1:
|
||||
self.parent().mainWindow.MESSAGEHANDLER.warning('Too many group items selected, '
|
||||
'please only choose one.', popUp=True)
|
||||
self.parent().mainWindow.setStatus('Too many group items selected, please only choose one.')
|
||||
else:
|
||||
# Only continue if just 1 group item is selected.
|
||||
groupEntityMaybe = selectedGroupItems[0]
|
||||
if not isinstance(groupEntityMaybe, Entity.GroupNode):
|
||||
self.parent().mainWindow.setStatus('Selected entity is not a group entity. Aborting adding new '
|
||||
@@ -1369,7 +1340,8 @@ class CanvasScene(QtWidgets.QGraphicsScene):
|
||||
# Add UIDs to list. If there are both links and nodes, remove
|
||||
# links from list and only keep nodes.
|
||||
selectedUIDs = [item.uid for item in self.selectedItems()
|
||||
if isinstance(item, Entity.BaseNode) or isinstance(item, Entity.BaseConnector)]
|
||||
if isinstance(item, (Entity.BaseNode, Entity.BaseConnector))]
|
||||
|
||||
self.parent().mainWindow.populateDetailsWidget(selectedUIDs)
|
||||
|
||||
def drawGraphOnCanvasFromOpen(self, canvasName: str) -> None:
|
||||
@@ -1387,8 +1359,9 @@ class CanvasScene(QtWidgets.QGraphicsScene):
|
||||
if steps == 0:
|
||||
return
|
||||
steps += 1
|
||||
progress = QtWidgets.QProgressDialog('Opening Canvas: ' + canvasName + ', please wait...',
|
||||
'', 0, steps, self.parent())
|
||||
progress = QtWidgets.QProgressDialog(f'Opening Canvas: {canvasName}, please wait...', '', 0, steps,
|
||||
self.parent())
|
||||
|
||||
# Remove Cancel button from progress bar (user should not be able to stop canvas from loading).
|
||||
progress.setMinimumDuration(1500)
|
||||
progress.setCancelButton(None)
|
||||
@@ -1440,7 +1413,7 @@ class CanvasScene(QtWidgets.QGraphicsScene):
|
||||
|
||||
progress.setValue(steps)
|
||||
self.adjustSceneRect()
|
||||
self.parent().mainWindow.MESSAGEHANDLER.info('Loaded canvas: ' + canvasName)
|
||||
self.parent().mainWindow.MESSAGEHANDLER.info(f'Loaded canvas: {canvasName}')
|
||||
|
||||
def updatePositionInDB(self, uid, x, y) -> None:
|
||||
"""
|
||||
@@ -1468,8 +1441,7 @@ class CanvasScene(QtWidgets.QGraphicsScene):
|
||||
# canvases will have different nodes on them.
|
||||
# The 'addNode' functions assume that they've been passed whatever entity group is the correct one, i.e.
|
||||
# either a new one being created or an old one that was copied.
|
||||
groupItems = [uid for uid in entity['Child UIDs'] if uid not in self.sceneGraph.nodes]
|
||||
if len(groupItems) > 0:
|
||||
if groupItems := [uid for uid in entity['Child UIDs'] if uid not in self.sceneGraph.nodes]:
|
||||
newNode = Entity.GroupNode(picture, uid, entity['Group Name'], self.entityTextFont,
|
||||
self.entityTextBrush)
|
||||
self.addNodeToScene(newNode, x, y)
|
||||
@@ -1518,19 +1490,17 @@ class CanvasScene(QtWidgets.QGraphicsScene):
|
||||
newNode = Entity.BaseNode(picture, uid, nodePrimaryAttribute, self.entityTextFont,
|
||||
self.entityTextBrush)
|
||||
self.addNodeToScene(newNode)
|
||||
else:
|
||||
groupItems = [uid for uid in groupItems if uid not in self.sceneGraph.nodes]
|
||||
if len(groupItems) > 0:
|
||||
newNode = Entity.GroupNode(picture, uid, entity['Group Name'], self.entityTextFont,
|
||||
self.entityTextBrush)
|
||||
self.addNodeToScene(newNode)
|
||||
elif groupItems := [uid for uid in groupItems if uid not in self.sceneGraph.nodes]:
|
||||
newNode = Entity.GroupNode(picture, uid, entity['Group Name'], self.entityTextFont,
|
||||
self.entityTextBrush)
|
||||
self.addNodeToScene(newNode)
|
||||
|
||||
newGroupList = newNode.listWidget
|
||||
newGroupListGraphic = self.addWidget(newGroupList)
|
||||
newGroupListGraphic.hide()
|
||||
newNode.formGroup(groupItems, newGroupListGraphic)
|
||||
for item in groupItems:
|
||||
self.sceneGraph.add_node(item, groupID=newNode.uid)
|
||||
newGroupList = newNode.listWidget
|
||||
newGroupListGraphic = self.addWidget(newGroupList)
|
||||
newGroupListGraphic.hide()
|
||||
newNode.formGroup(groupItems, newGroupListGraphic)
|
||||
for item in groupItems:
|
||||
self.sceneGraph.add_node(item, groupID=newNode.uid)
|
||||
if newNode is not None:
|
||||
if uid in self.sceneGraph.nodes:
|
||||
del self.sceneGraph.nodes[uid]['groupID']
|
||||
@@ -1554,7 +1524,7 @@ class CanvasScene(QtWidgets.QGraphicsScene):
|
||||
currGraphClone = self.sceneGraph.copy()
|
||||
|
||||
nodesToDel = set()
|
||||
for edgeParentUID, edgeChild in dict(currGraphClone.edges):
|
||||
for edgeParentUID, edgeChild in currGraphClone.edges:
|
||||
potentialGroupIDOne = currGraphClone.nodes[edgeParentUID].get('groupID', edgeParentUID)
|
||||
potentialGroupIDTwo = currGraphClone.nodes[edgeChild].get('groupID', edgeChild)
|
||||
if potentialGroupIDOne != edgeParentUID:
|
||||
@@ -1628,13 +1598,13 @@ class CanvasScene(QtWidgets.QGraphicsScene):
|
||||
nodesOnCanvas = {}
|
||||
for node in self.nodesDict:
|
||||
try:
|
||||
# Tiny differences in milliseconds are not considered to be significant.
|
||||
# Tiny differences in seconds are not considered to be significant.
|
||||
entityDate = datetime.fromisoformat(
|
||||
self.parent().entityDB.getEntity(node)['Date Created']).replace(microsecond=0)
|
||||
self.parent().entityDB.getEntity(node)['Date Created']).replace(microsecond=0, second=0)
|
||||
except (TypeError, ValueError):
|
||||
# Should never happen, but we will handle it if it does.
|
||||
self.parent().mainWindow.MESSAGEHANDLER.warning('Entity without valid Date Created: ' + str(node))
|
||||
entityDate = datetime.now().replace(microsecond=0)
|
||||
self.parent().mainWindow.MESSAGEHANDLER.warning(f'Entity without valid Date Created: {str(node)}')
|
||||
entityDate = datetime.now().replace(microsecond=0, second=0)
|
||||
if entityDate not in nodesOnCanvas:
|
||||
nodesOnCanvas[entityDate] = [node]
|
||||
else:
|
||||
@@ -1642,8 +1612,8 @@ class CanvasScene(QtWidgets.QGraphicsScene):
|
||||
sortedDates = sorted(nodesOnCanvas)
|
||||
xValue = 0
|
||||
yValue = 0
|
||||
for dateIndex in range(len(sortedDates)):
|
||||
for entityUID in nodesOnCanvas[sortedDates[dateIndex]]:
|
||||
for dateValue in sortedDates:
|
||||
for entityUID in nodesOnCanvas[dateValue]:
|
||||
self.scenePos[entityUID] = (xValue, yValue)
|
||||
self.nodesDict[entityUID].setPos(QtCore.QPointF(xValue, yValue))
|
||||
yValue += 150
|
||||
@@ -1798,7 +1768,7 @@ class CanvasScene(QtWidgets.QGraphicsScene):
|
||||
# Remove UIDs from list, and delete the link if no more UIDs are left
|
||||
self.linksDict[edgeToDelete].uid = {linkToStayUID for linkToStayUID in self.linksDict[edgeToDelete].uid
|
||||
if linkToStayUID not in edgesToDelete[edgeToDelete]}
|
||||
if len(self.linksDict[edgeToDelete].uid) == 0:
|
||||
if not self.linksDict[edgeToDelete].uid:
|
||||
self.removeEdge(self.linksDict[edgeToDelete])
|
||||
|
||||
def removeUIDFromLink(self, linkUIDToRemove: tuple) -> None:
|
||||
@@ -1869,10 +1839,8 @@ class CanvasScene(QtWidgets.QGraphicsScene):
|
||||
childLinks = self.parent().entityDB.getOutgoingLinks(item)
|
||||
for childLink in childLinks:
|
||||
# Ask forgiveness instead of permission - set children as selected, if they are on the canvas.
|
||||
try:
|
||||
with contextlib.suppress(KeyError):
|
||||
self.nodesDict[childLink[1]].setSelected(True)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def selectParentNodes(self) -> None:
|
||||
items = [item.uid for item in self.selectedItems() if isinstance(item, Entity.BaseNode)]
|
||||
@@ -1881,10 +1849,8 @@ class CanvasScene(QtWidgets.QGraphicsScene):
|
||||
parentLinks = self.parent().entityDB.getIncomingLinks(item)
|
||||
for parentLink in parentLinks:
|
||||
# Ask forgiveness instead of permission - set parents as selected, if they are on the canvas.
|
||||
try:
|
||||
with contextlib.suppress(KeyError):
|
||||
self.nodesDict[parentLink[0]].setSelected(True)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
# Because the entities on each canvas are stored in dicts, and dicts are ordered, group nodes will always
|
||||
# come after the nodes they contain.
|
||||
@@ -1934,11 +1900,9 @@ class CanvasScene(QtWidgets.QGraphicsScene):
|
||||
def deleteSelectedItems(self) -> None:
|
||||
items = self.selectedItems()
|
||||
for item in items:
|
||||
if isinstance(item, Entity.BaseNode):
|
||||
if isinstance(item, Entity.GroupNode):
|
||||
if item.listProxyWidget is not None:
|
||||
item.listProxyWidget.hide()
|
||||
self.removeNode(item)
|
||||
if isinstance(item, Entity.GroupNode) and item.listProxyWidget is not None:
|
||||
item.listProxyWidget.hide()
|
||||
self.removeNode(item)
|
||||
|
||||
def removeNode(self, nodeItem: Entity.BaseNode) -> None:
|
||||
if not isinstance(nodeItem, Entity.BaseNode) or self.sceneGraph.nodes.get(nodeItem.uid) is None:
|
||||
@@ -1953,14 +1917,10 @@ class CanvasScene(QtWidgets.QGraphicsScene):
|
||||
self.clearSelection()
|
||||
self.removeItem(nodeItem)
|
||||
# Node already deleted
|
||||
try:
|
||||
with contextlib.suppress(nx.exception.NetworkXError):
|
||||
self.sceneGraph.remove_node(uid)
|
||||
except nx.exception.NetworkXError:
|
||||
pass
|
||||
try:
|
||||
with contextlib.suppress(KeyError):
|
||||
self.scenePos.pop(uid)
|
||||
except KeyError:
|
||||
pass
|
||||
if isinstance(nodeItem, Entity.GroupNode):
|
||||
for childUID in nodeItem.groupedNodesUid:
|
||||
self.sceneGraph.remove_node(childUID)
|
||||
@@ -1968,22 +1928,18 @@ class CanvasScene(QtWidgets.QGraphicsScene):
|
||||
del nodeItem
|
||||
|
||||
def removeEdge(self, edgeItem: Entity.BaseConnector) -> None:
|
||||
try:
|
||||
with contextlib.suppress(KeyError):
|
||||
# KeyError means that the edge was already removed from linksDict.
|
||||
self.linksDict.pop(edgeItem.myStartItem.uid + edgeItem.myEndItem.uid)
|
||||
except KeyError:
|
||||
# Edge already removed from linksDict.
|
||||
pass
|
||||
uidProper = (edgeItem.myStartItem.uid, edgeItem.myEndItem.uid)
|
||||
edgeItem.hide()
|
||||
edgeItem.myStartItem.removeConnector(self)
|
||||
edgeItem.myEndItem.removeConnector(self)
|
||||
if edgeItem in self.selectedItems():
|
||||
self.clearSelection()
|
||||
try:
|
||||
with contextlib.suppress(nx.exception.NetworkXError):
|
||||
# NetworkXError is thrown when an edge is already deleted.
|
||||
self.sceneGraph.remove_edge(uidProper[0], uidProper[1])
|
||||
except nx.exception.NetworkXError:
|
||||
# Thrown when an edge is already deleted.
|
||||
pass
|
||||
# Could be the case that some link graphics are duplicated or left hanging.
|
||||
# This would clear them out.
|
||||
if edgeItem.scene() == self:
|
||||
@@ -1996,14 +1952,7 @@ class CanvasScene(QtWidgets.QGraphicsScene):
|
||||
"""
|
||||
newNodes = [node for node in canvas_nodes if node not in self.nodesDict and
|
||||
self.parent().mainWindow.LENTDB.getEntity(node)['Entity Type'] != 'EntityGroup']
|
||||
# newGroupNodes = [y for y in newNodes
|
||||
# if self.parent().mainWindow.LENTDB.getEntity(y)['Entity Type'] == 'EntityGroup']
|
||||
# newNodesInGroup = []
|
||||
# for groupNode in newGroupNodes:
|
||||
# entityJson = self.parent().mainWindow.LENTDB.getEntity(groupNode)
|
||||
# self.addNodeProgrammatic(groupNode, entityJson['Child UIDs'], fromServer=True)
|
||||
# newNodesInGroup += entityJson['Child UIDs']
|
||||
for node in newNodes: # [z for z in newNodes if z not in newNodesInGroup]:
|
||||
for node in newNodes:
|
||||
self.addNodeProgrammatic(node, fromServer=True)
|
||||
|
||||
# Edges technically only added if the related nodes are already created,
|
||||
@@ -2021,7 +1970,6 @@ class PropertiesEditor(QtWidgets.QDialog):
|
||||
self.setModal(True)
|
||||
self.isEditingNode = isNode
|
||||
|
||||
# self.setLayout(QtWidgets.QGridLayout())
|
||||
self.setWindowTitle("Properties Editor")
|
||||
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
|
||||
self.setMinimumSize(500, 300)
|
||||
@@ -2089,7 +2037,7 @@ class PropertiesEditor(QtWidgets.QDialog):
|
||||
if isValid is True:
|
||||
super().accept()
|
||||
elif isinstance(isValid, str):
|
||||
self.canvas.parent().mainWindow.MESSAGEHANDLER.error('Entity fields contain invalid values: ' + isValid,
|
||||
self.canvas.parent().mainWindow.MESSAGEHANDLER.error(f'Entity fields contain invalid values: {isValid}',
|
||||
exc_info=False)
|
||||
else:
|
||||
self.canvas.parent().mainWindow.MESSAGEHANDLER.error('Error occurred when checking validity of entity '
|
||||
@@ -2162,8 +2110,7 @@ class PropertiesEditorIconField(QtWidgets.QLabel):
|
||||
except ValueError as ve:
|
||||
# Image type is unsupported (for ImageQt)
|
||||
# Supported types: 1, L, P, RGB, RGBA
|
||||
self.canvas.parent().mainWindow.MESSAGEHANDLER.warning(
|
||||
'Invalid Image selected: ' + str(ve), popUp=True)
|
||||
self.canvas.parent().mainWindow.MESSAGEHANDLER.warning(f'Invalid Image selected: {str(ve)}', popUp=True)
|
||||
|
||||
super(PropertiesEditorIconField, self).mousePressEvent(event)
|
||||
|
||||
|
||||
@@ -352,7 +352,7 @@ class ResolutionList(QtWidgets.QTreeWidget):
|
||||
if resolution not in self.resolutionManager.getResolutionsInCategory(category):
|
||||
return
|
||||
|
||||
self.mainWindow.runResolution(category + '/' + resolution)
|
||||
self.mainWindow.runResolution(f'{category}/{resolution}')
|
||||
|
||||
|
||||
class ResolutionWidget(QtWidgets.QTreeWidgetItem):
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import contextlib
|
||||
from PySide6 import QtWidgets, QtCore, QtCharts, QtGui
|
||||
from Core.Interface import Stylesheets
|
||||
from datetime import datetime
|
||||
@@ -129,16 +130,12 @@ class TimeWidget(QtWidgets.QWidget):
|
||||
|
||||
if nodeMinute not in self.timeDetails[nodeYear][nodeMonth][nodeDay][nodeHour]:
|
||||
# Sanity check. Should not be able to remove nodes that do not exist, but you never know.
|
||||
if added:
|
||||
self.timeDetails[nodeYear][nodeMonth][nodeDay][nodeHour][nodeMinute] = 1
|
||||
else:
|
||||
self.timeDetails[nodeYear][nodeMonth][nodeDay][nodeHour][nodeMinute] = 0
|
||||
else:
|
||||
if added:
|
||||
self.timeDetails[nodeYear][nodeMonth][nodeDay][nodeHour][nodeMinute] += 1
|
||||
else:
|
||||
if self.timeDetails[nodeYear][nodeMonth][nodeDay][nodeHour][nodeMinute] > 0:
|
||||
self.timeDetails[nodeYear][nodeMonth][nodeDay][nodeHour][nodeMinute] -= 1
|
||||
self.timeDetails[nodeYear][nodeMonth][nodeDay][nodeHour][nodeMinute] = 1 if added else 0
|
||||
|
||||
elif added:
|
||||
self.timeDetails[nodeYear][nodeMonth][nodeDay][nodeHour][nodeMinute] += 1
|
||||
elif self.timeDetails[nodeYear][nodeMonth][nodeDay][nodeHour][nodeMinute] > 0:
|
||||
self.timeDetails[nodeYear][nodeMonth][nodeDay][nodeHour][nodeMinute] -= 1
|
||||
|
||||
if updateGraph:
|
||||
self.drawChart([])
|
||||
@@ -177,15 +174,11 @@ class TimeWidget(QtWidgets.QWidget):
|
||||
if minute is not None:
|
||||
barsDict = {minute: self.timeDetails[year][month][day][hour][minute]}
|
||||
self.currentTimeStep = [year, month, day, hour, minute]
|
||||
self.drawChartHelper(barsDict, self.currentTimeStep)
|
||||
|
||||
elif hour is not None:
|
||||
barsDict = {}
|
||||
for minute in self.timeDetails[year][month][day][hour]:
|
||||
barsDict[minute] = self.timeDetails[year][month][day][hour][minute]
|
||||
self.currentTimeStep = [year, month, day, hour]
|
||||
self.drawChartHelper(barsDict, self.currentTimeStep)
|
||||
barsDict = {minute: self.timeDetails[year][month][day][hour][minute]
|
||||
for minute in self.timeDetails[year][month][day][hour]}
|
||||
|
||||
self.currentTimeStep = [year, month, day, hour]
|
||||
elif day is not None:
|
||||
barsDict = {}
|
||||
for hour in self.timeDetails[year][month][day]:
|
||||
@@ -193,8 +186,6 @@ class TimeWidget(QtWidgets.QWidget):
|
||||
for minute in self.timeDetails[year][month][day][hour]:
|
||||
barsDict[hour] += self.timeDetails[year][month][day][hour][minute]
|
||||
self.currentTimeStep = [year, month, day]
|
||||
self.drawChartHelper(barsDict, self.currentTimeStep)
|
||||
|
||||
elif month is not None:
|
||||
barsDict = {}
|
||||
for day in self.timeDetails[year][month]:
|
||||
@@ -203,8 +194,6 @@ class TimeWidget(QtWidgets.QWidget):
|
||||
for minute in self.timeDetails[year][month][day][hour]:
|
||||
barsDict[day] += self.timeDetails[year][month][day][hour][minute]
|
||||
self.currentTimeStep = [year, month]
|
||||
self.drawChartHelper(barsDict, self.currentTimeStep)
|
||||
|
||||
elif year is not None:
|
||||
barsDict = {}
|
||||
for month in self.timeDetails[year]:
|
||||
@@ -214,8 +203,6 @@ class TimeWidget(QtWidgets.QWidget):
|
||||
for minute in self.timeDetails[year][month][day][hour]:
|
||||
barsDict[month] += self.timeDetails[year][month][day][hour][minute]
|
||||
self.currentTimeStep = [year]
|
||||
self.drawChartHelper(barsDict, self.currentTimeStep)
|
||||
|
||||
else:
|
||||
barsDict = {}
|
||||
for year in self.timeDetails:
|
||||
@@ -226,7 +213,8 @@ class TimeWidget(QtWidgets.QWidget):
|
||||
for minute in self.timeDetails[year][month][day][hour]:
|
||||
barsDict[year] += self.timeDetails[year][month][day][hour][minute]
|
||||
self.currentTimeStep = []
|
||||
self.drawChartHelper(barsDict, self.currentTimeStep)
|
||||
|
||||
self.drawChartHelper(barsDict, self.currentTimeStep)
|
||||
|
||||
def drawChartHelper(self, barsDict: dict, timestep: list):
|
||||
timelineSeries = QtCharts.QBarSeries(self.timelineChart)
|
||||
@@ -241,14 +229,13 @@ class TimeWidget(QtWidgets.QWidget):
|
||||
if barsDict[bar] > maxEntityNum:
|
||||
maxEntityNum = barsDict[bar]
|
||||
value = ""
|
||||
for step in range(len(timestep)):
|
||||
for step, stepValue in enumerate(timestep):
|
||||
if step <= 2:
|
||||
value += str(timestep[step]) + '/'
|
||||
value += f'{str(stepValue)}/'
|
||||
if step == 2:
|
||||
value = value[:-1]
|
||||
value += " "
|
||||
value = f"{value[:-1]} "
|
||||
else:
|
||||
value += str(timestep[step]) + ':'
|
||||
value += f'{str(stepValue)}:'
|
||||
value += str(bar)
|
||||
xAxisValues.append(value)
|
||||
self.timelineChart.removeAllSeries()
|
||||
@@ -276,10 +263,7 @@ class TimeWidget(QtWidgets.QWidget):
|
||||
self.timescaleSelector.adjustLabelsToHour()
|
||||
elif timesteps == 4:
|
||||
self.timescaleSelector.adjustLabelsToMinute()
|
||||
elif timesteps == 5:
|
||||
self.timescaleSelector.adjustLabelsToSecond()
|
||||
else:
|
||||
# Just in case.
|
||||
self.timescaleSelector.adjustLabelsToSecond()
|
||||
|
||||
yAxis.applyNiceNumbers()
|
||||
@@ -501,7 +485,7 @@ class ChatBox(QtWidgets.QWidget):
|
||||
self.setMinimumWidth(500)
|
||||
self.setMaximumWidth(500)
|
||||
|
||||
self.chatName = getuser() + ": "
|
||||
self.chatName = f"{getuser()}: "
|
||||
|
||||
chatLayout = QtWidgets.QGridLayout()
|
||||
self.setLayout(chatLayout)
|
||||
@@ -536,14 +520,10 @@ class LoggingUpdateThread(QtCore.QThread):
|
||||
self.messageHandler = messageHandler
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
if self.endLogging:
|
||||
break
|
||||
while not self.endLogging:
|
||||
if not self.messageHandler.logQueue.empty():
|
||||
try:
|
||||
with contextlib.suppress(queue.Empty):
|
||||
logMsg = self.messageHandler.logQueue.get().getMessage()
|
||||
self.loggingSignal.emit(logMsg)
|
||||
except queue.Empty:
|
||||
pass
|
||||
else:
|
||||
self.msleep(100)
|
||||
|
||||
@@ -50,13 +50,10 @@ class DockBarTwo(QtWidgets.QDockWidget):
|
||||
self.initialiseLayout()
|
||||
|
||||
def setNotesText(self, newText: str) -> None:
|
||||
if self.tabNotes.textEditor.isReadOnly():
|
||||
self.tabNotes.textEditor.contents = newText
|
||||
self.tabNotes.textEditor.setMarkdown(newText)
|
||||
else:
|
||||
if not self.tabNotes.textEditor.isReadOnly():
|
||||
self.tabNotes.textEditor.setReadOnly(True)
|
||||
self.tabNotes.textEditor.contents = newText
|
||||
self.tabNotes.textEditor.setMarkdown(newText)
|
||||
self.tabNotes.textEditor.contents = newText
|
||||
self.tabNotes.textEditor.setMarkdown(newText)
|
||||
|
||||
def getNotesText(self) -> str:
|
||||
"""
|
||||
@@ -248,15 +245,15 @@ class EntityDetails(QtWidgets.QWidget):
|
||||
else:
|
||||
self.populateMultiRelationshipHelperLink(item)
|
||||
linksNumber += 1
|
||||
self.multiNodesTableLabelOne.setText('Selected Nodes: ' + str(nodesNumber))
|
||||
self.multiNodesTableLabelTwo.setText('Selected Links: ' + str(linksNumber))
|
||||
self.multiNodesTableLabelOne.setText(f'Selected Nodes: {str(nodesNumber)}')
|
||||
self.multiNodesTableLabelTwo.setText(f'Selected Links: {str(linksNumber)}')
|
||||
|
||||
self.switchLayoutHelper(numberOfItems, isNode)
|
||||
except Exception as exc:
|
||||
# If an error is thrown at some point during the process, show the default nothing selected screen.
|
||||
self.detailsLayout.setCurrentIndex(0)
|
||||
self.mainWindow.MESSAGEHANDLER.error('Error occurred while trying to display the details of the selected '
|
||||
'nodes: ' + str(exc), popUp=False, exc_info=False)
|
||||
self.mainWindow.MESSAGEHANDLER.error(f'Error occurred while trying to display the details of the selected '
|
||||
f'nodes: {str(exc)}', popUp=False, exc_info=False)
|
||||
|
||||
# Display helper functions
|
||||
def clearDetailsHelper(self) -> None:
|
||||
@@ -279,7 +276,7 @@ class EntityDetails(QtWidgets.QWidget):
|
||||
return
|
||||
rowCount = 0
|
||||
for key in jsonDict:
|
||||
if key == "uid" or key == "Child UIDs" or key == "Icon":
|
||||
if key in ["uid", "Child UIDs", "Icon"]:
|
||||
continue
|
||||
elif key == "Notes":
|
||||
notesTextArea = RichNotesEditor(self, jsonDict[key], False)
|
||||
@@ -339,7 +336,7 @@ class EntityDetails(QtWidgets.QWidget):
|
||||
nodePixmap,
|
||||
edgeJson[list(edgeJson)[1]],
|
||||
uid)
|
||||
self.relationshipsIncomingTable.setHeaderLabel('Incoming Links: ' + str(len(inc)))
|
||||
self.relationshipsIncomingTable.setHeaderLabel(f'Incoming Links: {len(inc)}')
|
||||
for edge in out:
|
||||
uid = edge[1]
|
||||
edgeJson = self.entityDB.getEntity(uid)
|
||||
@@ -349,7 +346,7 @@ class EntityDetails(QtWidgets.QWidget):
|
||||
nodePixmap,
|
||||
edgeJson[list(edgeJson)[1]],
|
||||
uid)
|
||||
self.relationshipsOutgoingTable.setHeaderLabel('Outgoing Links: ' + str(len(out)))
|
||||
self.relationshipsOutgoingTable.setHeaderLabel(f'Outgoing Links: {len(out)}')
|
||||
|
||||
def populateMultiRelationshipHelperNode(self, nodeJson) -> None:
|
||||
inc = len(self.entityDB.getIncomingLinks(nodeJson['uid']))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import contextlib
|
||||
from json import dumps
|
||||
import math
|
||||
from typing import Any, Optional
|
||||
@@ -69,18 +70,16 @@ class BaseNode(QGraphicsItemGroup):
|
||||
|
||||
def updateLabel(self, newText: str = '') -> None:
|
||||
if not isinstance(newText, str):
|
||||
newText = str(newText)
|
||||
newText = newText
|
||||
if newText != '':
|
||||
if len(newText) > 50:
|
||||
newText = newText[:47] + "..."
|
||||
newText = f"{newText[:47]}..."
|
||||
self.labelItem.setPlainText(newText)
|
||||
|
||||
def removeConnector(self, connector) -> None:
|
||||
# Exception could be thrown if the connector is already deleted.
|
||||
try:
|
||||
with contextlib.suppress(ValueError):
|
||||
self.connectors.remove(connector)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def addConnector(self, connector) -> None:
|
||||
self.connectors.append(connector)
|
||||
@@ -235,10 +234,7 @@ class BaseConnector(QGraphicsItemGroup):
|
||||
self.updateLabel(name)
|
||||
|
||||
if uid is not None:
|
||||
if isinstance(uid, list) or isinstance(uid, set):
|
||||
self.uid = set(uid)
|
||||
else:
|
||||
self.uid = {uid}
|
||||
self.uid = set(uid) if isinstance(uid, (list, set)) else {uid}
|
||||
else:
|
||||
self.uid = {(origin.uid, destination.uid)}
|
||||
|
||||
@@ -262,7 +258,7 @@ class BaseConnector(QGraphicsItemGroup):
|
||||
|
||||
def updateLabel(self, newText: str = '') -> None:
|
||||
if len(newText) > 50:
|
||||
newText = newText[:47] + "..."
|
||||
newText = f"{newText[:47]}..."
|
||||
self.labelItem.setText(newText)
|
||||
self.update()
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
import contextlib
|
||||
import hashlib
|
||||
import re
|
||||
import json
|
||||
@@ -508,11 +509,9 @@ class MenuBar(QtWidgets.QMenuBar):
|
||||
fileContents = []
|
||||
with open(fileDirectory, 'r') as importFile:
|
||||
# Read a maximum of 3 lines from the file:
|
||||
count = 0
|
||||
for line in importFile:
|
||||
for index, line in enumerate(importFile):
|
||||
fileContents.append(line.strip())
|
||||
count += 1
|
||||
if count >= 3:
|
||||
if index > 2:
|
||||
break
|
||||
|
||||
importTextFileDialog = ImportFromTextFileDialog(self, fileContents)
|
||||
@@ -561,10 +560,10 @@ class MenuBar(QtWidgets.QMenuBar):
|
||||
importEntityCSVDialog = ImportEntityFromCSVFile(self, csvDF)
|
||||
if importEntityCSVDialog.exec_():
|
||||
attributeRows = [comboBox.currentText()
|
||||
for comboBox in importEntityCSVDialog.fieldMappingComboBoxes]
|
||||
for attribute in range(len(attributeRows)):
|
||||
if attributeRows[attribute] == '':
|
||||
attributeRows[attribute] = csvDF.columns[attribute]
|
||||
if comboBox.currentText() else
|
||||
csvDF.columns[index]
|
||||
for index, comboBox in
|
||||
enumerate(importEntityCSVDialog.fieldMappingComboBoxes)]
|
||||
|
||||
if importEntityCSVDialog.importToCanvasCheckbox.isChecked():
|
||||
sceneToAddTo = self.parent().centralWidget().tabbedPane.getSceneByName(
|
||||
@@ -572,8 +571,8 @@ class MenuBar(QtWidgets.QMenuBar):
|
||||
|
||||
entityTypeToImportAs = importEntityCSVDialog.entityTypeChoiceDropdown.currentText()
|
||||
for row in csvDF.itertuples(index=False):
|
||||
newEntityJSON = {str(attributeRows[key]).strip(): str(row[key]).strip()
|
||||
for key in range(len(attributeRows))}
|
||||
newEntityJSON = {str(value).strip(): str(row[index]).strip()
|
||||
for index, value in enumerate(attributeRows)}
|
||||
newEntityJSON['Entity Type'] = entityTypeToImportAs
|
||||
if newEntityJSON not in newNodes:
|
||||
primaryAttr = newEntityJSON[
|
||||
@@ -608,9 +607,9 @@ class MenuBar(QtWidgets.QMenuBar):
|
||||
importLinksCSVDialog = ImportLinksFromCSVFile(self, csvDF)
|
||||
if importLinksCSVDialog.exec_():
|
||||
unmapped = []
|
||||
for columnIndex in range(len(importLinksCSVDialog.fieldMappingComboBoxes)):
|
||||
columnMapping = importLinksCSVDialog.fieldMappingComboBoxes[columnIndex].currentText()
|
||||
if columnMapping != '':
|
||||
for columnIndex, columnValue in enumerate(importLinksCSVDialog.fieldMappingComboBoxes):
|
||||
columnMapping = columnValue.currentText()
|
||||
if columnMapping:
|
||||
csvDF.rename(columns={csvDF.columns[columnIndex]: columnMapping}, inplace=True)
|
||||
elif not importLinksCSVDialog.fieldIncludeCheckBoxes[columnIndex].isChecked():
|
||||
unmapped.append(csvDF.columns[columnIndex])
|
||||
@@ -628,10 +627,10 @@ class MenuBar(QtWidgets.QMenuBar):
|
||||
entityTwoType = importLinksCSVDialog.entityTwoTypeChoiceDropdown.currentText()
|
||||
|
||||
attributeRows = [comboBox.currentText()
|
||||
for comboBox in createLinkEntitiesDialog.fieldMappingComboBoxes]
|
||||
for attribute in range(len(attributeRows)):
|
||||
if attributeRows[attribute] == '':
|
||||
attributeRows[attribute] = fieldsRemainingDF.columns[attribute]
|
||||
if comboBox.currentText()
|
||||
else fieldsRemainingDF.columns[index]
|
||||
for index, comboBox
|
||||
in enumerate(createLinkEntitiesDialog.fieldMappingComboBoxes)]
|
||||
|
||||
entityTypeToImportAs = \
|
||||
createLinkEntitiesDialog.entityTypeChoiceDropdown.currentText()
|
||||
@@ -642,14 +641,13 @@ class MenuBar(QtWidgets.QMenuBar):
|
||||
for entityRow, linkRow in zip(fieldsRemainingDF.itertuples(index=False),
|
||||
csvDF.itertuples(index=False)):
|
||||
|
||||
count = 0
|
||||
linkJSON = {}
|
||||
entityOneJSON = {}
|
||||
entityTwoJSON = {}
|
||||
resolutionID = ""
|
||||
notes = ""
|
||||
|
||||
for column in linkRow:
|
||||
for count, column in enumerate(linkRow):
|
||||
column = str(column)
|
||||
mapping = csvDF.columns[count]
|
||||
if mapping == 'Entity One':
|
||||
@@ -674,7 +672,6 @@ class MenuBar(QtWidgets.QMenuBar):
|
||||
resolutionID = column
|
||||
else:
|
||||
linkJSON[mapping] = column
|
||||
count += 1
|
||||
|
||||
# We still need to check if both nodes exist, since errors may have occurred
|
||||
# during their creation.
|
||||
@@ -696,12 +693,12 @@ class MenuBar(QtWidgets.QMenuBar):
|
||||
linkJSONTwo = dict(linkJSON)
|
||||
linkJSONTwo['Resolution'] += ' IN'
|
||||
|
||||
newEntityJSON = {str(attributeRows[key]): str(entityRow[key]).strip()
|
||||
for key in range(len(attributeRows))}
|
||||
newEntityJSON = {str(value): str(entityRow[index]).strip()
|
||||
for index, value in enumerate(attributeRows)}
|
||||
newEntityJSON['Entity Type'] = entityTypeToImportAs
|
||||
|
||||
if randomizePrimary:
|
||||
newEntityJSON[newEntityPrimaryAttribute] += ' | ' + str(uuid4())
|
||||
newEntityJSON[newEntityPrimaryAttribute] += f' | {str(uuid4())}'
|
||||
|
||||
newNode = self.parent().LENTDB.getEntityOfType(
|
||||
newEntityJSON[newEntityPrimaryAttribute], entityTypeToImportAs)
|
||||
@@ -734,14 +731,13 @@ class MenuBar(QtWidgets.QMenuBar):
|
||||
entityTwoType = importLinksCSVDialog.entityTwoTypeChoiceDropdown.currentText()
|
||||
|
||||
for row in csvDF.itertuples(index=False):
|
||||
count = 0
|
||||
linkJSON = {}
|
||||
entityOneJSON = {}
|
||||
entityTwoJSON = {}
|
||||
resolutionID = ""
|
||||
notes = ""
|
||||
|
||||
for column in row:
|
||||
for count, column in enumerate(row):
|
||||
column = str(column)
|
||||
mapping = csvDF.columns[count]
|
||||
if mapping == 'Entity One':
|
||||
@@ -756,7 +752,6 @@ class MenuBar(QtWidgets.QMenuBar):
|
||||
resolutionID = column
|
||||
else:
|
||||
linkJSON[mapping] = column
|
||||
count += 1
|
||||
|
||||
if (entityOneJSON is not None) and (entityTwoJSON is not None):
|
||||
linkJSON['uid'] = (entityOneJSON['uid'], entityTwoJSON['uid'])
|
||||
@@ -815,14 +810,8 @@ class MenuBar(QtWidgets.QMenuBar):
|
||||
|
||||
if canvasSaveDialogAccept and fileDirectory != '':
|
||||
canvas = canvasSaveDialog.chosenCanvasDropdown.currentText()
|
||||
if canvasSaveDialog.justViewportChoice.isChecked():
|
||||
justViewport = True
|
||||
else:
|
||||
justViewport = False
|
||||
if canvasSaveDialog.transparentChoice.isChecked():
|
||||
transparentBackground = True
|
||||
else:
|
||||
transparentBackground = False
|
||||
justViewport = canvasSaveDialog.justViewportChoice.isChecked()
|
||||
transparentBackground = canvasSaveDialog.transparentChoice.isChecked()
|
||||
picture = self.parent().getPictureOfCanvas(canvas, justViewport, transparentBackground)
|
||||
picture.save(fileDirectory, "PNG")
|
||||
|
||||
@@ -957,10 +946,7 @@ class MenuBar(QtWidgets.QMenuBar):
|
||||
self.parent().FCOM.askServerForFileList(project_name)
|
||||
|
||||
def forceDatabaseSync(self) -> None:
|
||||
if self.parent().FCOM.isConnected():
|
||||
project_name = self.parent().SETTINGS.value("Project/Server/Project")
|
||||
with self.parent().LENTDB.dbLock:
|
||||
self.parent().FCOM.syncDatabase(project_name, self.parent().LENTDB.database)
|
||||
self.parent().syncDatabase()
|
||||
|
||||
def uploadFiles(self) -> None:
|
||||
self.parent().uploadFiles()
|
||||
@@ -1036,10 +1022,8 @@ class MenuBar(QtWidgets.QMenuBar):
|
||||
selectedURLs = SearchEngineDialog(self)
|
||||
|
||||
if selectedURLs.exec_():
|
||||
selectedEngineURLs = []
|
||||
for engineCheckbox in selectedURLs.searchEngineWidgets:
|
||||
if engineCheckbox.isChecked():
|
||||
selectedEngineURLs.append(engineCheckbox.searchAttr)
|
||||
selectedEngineURLs = [engineCheckbox.searchAttr for engineCheckbox in selectedURLs.searchEngineWidgets
|
||||
if engineCheckbox.isChecked()]
|
||||
searchTerms = []
|
||||
for item in currentScene.selectedItems():
|
||||
if isinstance(item, BaseNode):
|
||||
@@ -1057,10 +1041,8 @@ class MenuBar(QtWidgets.QMenuBar):
|
||||
selectedURLs = SearchImageEngineDialog(self)
|
||||
|
||||
if selectedURLs.exec_():
|
||||
selectedEngineURLs = []
|
||||
for engineCheckbox in selectedURLs.searchEngineWidgets:
|
||||
if engineCheckbox.isChecked():
|
||||
selectedEngineURLs.append(engineCheckbox.searchAttr)
|
||||
selectedEngineURLs = [engineCheckbox.searchAttr for engineCheckbox in selectedURLs.searchEngineWidgets
|
||||
if engineCheckbox.isChecked()]
|
||||
for searchEngineURL in selectedEngineURLs:
|
||||
try:
|
||||
QtGui.QDesktopServices.openUrl(searchEngineURL)
|
||||
@@ -1092,7 +1074,7 @@ class MenuBar(QtWidgets.QMenuBar):
|
||||
except KeyError:
|
||||
continue
|
||||
|
||||
if len(websiteEntities) == 0:
|
||||
if not websiteEntities:
|
||||
self.parent().MESSAGEHANDLER.warning('Please select the "Website" nodes that correspond to the sites that '
|
||||
'you wish to download, and re-run the Download Selected Websites '
|
||||
'operation.',
|
||||
@@ -1121,7 +1103,7 @@ class MenuBar(QtWidgets.QMenuBar):
|
||||
except KeyError:
|
||||
continue
|
||||
|
||||
if len(websiteEntities) == 0:
|
||||
if not websiteEntities:
|
||||
self.parent().MESSAGEHANDLER.warning('Please select the "Website" nodes that correspond to the sites that '
|
||||
'you wish to download, and re-run the Download Selected Websites '
|
||||
'operation.',
|
||||
@@ -1153,9 +1135,8 @@ class MenuBar(QtWidgets.QMenuBar):
|
||||
fileName = itemPrimaryField + ' | ' + itemJSON.get('Date Last Edited', str(time.time_ns())) + '.txt'
|
||||
fileName = fileName.replace('/', '+')
|
||||
fileName = fileName.replace('\\', '+')
|
||||
f = open(baseFilesPath / fileName, "w")
|
||||
f.write(itemJSONNotes)
|
||||
f.close()
|
||||
with open(baseFilesPath / fileName, "w") as f:
|
||||
f.write(itemJSONNotes)
|
||||
newNodes.append([{'Document Name': fileName,
|
||||
'File Path': fileName,
|
||||
'Entity Type': 'Document'},
|
||||
@@ -1172,7 +1153,7 @@ class MenuBar(QtWidgets.QMenuBar):
|
||||
|
||||
:return:
|
||||
"""
|
||||
if platform.system() != 'Linux' and platform.system() != 'Windows':
|
||||
if platform.system() not in ['Linux', 'Windows']:
|
||||
self.parent().setStatus('Importing tabs not supported on platforms other than Linux and Windows.')
|
||||
self.parent().MESSAGEHANDLER.warning('Importing tabs not supported on platforms '
|
||||
'other than Linux and Windows.', popUp=True)
|
||||
@@ -1279,7 +1260,7 @@ class MenuBar(QtWidgets.QMenuBar):
|
||||
del newNodeJSON[newNodePrimaryFieldKey]
|
||||
try:
|
||||
notesField = newNodeJSON.pop('Notes')
|
||||
existingEntityJSON['Notes'] += '\n' + notesField
|
||||
existingEntityJSON['Notes'] += f'\n{notesField}'
|
||||
except KeyError:
|
||||
# If no new field was actually added to the entity, don't re-add to the database
|
||||
if len(newNodeJSON) == 0:
|
||||
@@ -1303,10 +1284,9 @@ class MenuBar(QtWidgets.QMenuBar):
|
||||
allEntityUIDs.append(entityJson['uid'])
|
||||
allEntityPrimaryFieldsAndTypes.append((newNodePrimaryField, newNodeEntityType))
|
||||
|
||||
for resultListIndex in range(len(resolution_result)):
|
||||
if len(resolution_result[resultListIndex]) > 1:
|
||||
outputEntityUID = newNodeUIDs[resultListIndex]
|
||||
parentsDict = resolution_result[resultListIndex][1]
|
||||
for parentsDictHolder, outputEntityUID in zip(resolution_result, newNodeUIDs):
|
||||
if len(parentsDictHolder) > 1:
|
||||
parentsDict = parentsDictHolder[1]
|
||||
for parentID in parentsDict:
|
||||
parentUID = parentID
|
||||
if isinstance(parentUID, int):
|
||||
@@ -1319,7 +1299,7 @@ class MenuBar(QtWidgets.QMenuBar):
|
||||
if newLinkUID in allLinks:
|
||||
linkJson = self.parent().LENTDB.getLinkIfExists(newLinkUID)
|
||||
if resolutionName not in linkJson['Notes']:
|
||||
linkJson['Notes'] += '\nConnection also produced by Resolution: ' + resolutionName
|
||||
linkJson['Notes'] += f'\nConnection also produced by Resolution: {resolutionName}'
|
||||
self.parent().LENTDB.addLink(linkJson, fromServer=True)
|
||||
else:
|
||||
self.parent().LENTDB.addLink({'uid': newLinkUID, 'Resolution': resolutionName,
|
||||
@@ -1351,7 +1331,7 @@ class DeleteProjectConfirmationDialog(QtWidgets.QDialog):
|
||||
self.mainWindowObject = mainWindowObject
|
||||
self.setWindowTitle('Delete Server Project')
|
||||
|
||||
resolutionsLabel = QtWidgets.QLabel('Delete Project: "' + currentServerProject + '" ?')
|
||||
resolutionsLabel = QtWidgets.QLabel(f'Delete Project: "{currentServerProject}" ?')
|
||||
resolutionsLabel.setWordWrap(True)
|
||||
|
||||
resolutionsLabel.setAlignment(QtCore.Qt.AlignCenter)
|
||||
@@ -1626,7 +1606,7 @@ class CollectorsDialog(QtWidgets.QDialog):
|
||||
self.connectedToServerFormWidgetLayout = QtWidgets.QVBoxLayout()
|
||||
|
||||
for category in collectorsDict:
|
||||
categoryLabel = QtWidgets.QLabel("Category: " + str(category))
|
||||
categoryLabel = QtWidgets.QLabel(f"Category: {str(category)}")
|
||||
self.connectedToServerFormWidgetLayout.addWidget(categoryLabel)
|
||||
for collector in collectorsDict[category]:
|
||||
newCollectorWidget = QtWidgets.QWidget()
|
||||
@@ -1651,8 +1631,8 @@ class CollectorsDialog(QtWidgets.QDialog):
|
||||
newCollectorWidgetLayout.addWidget(newCollectorInstanceTree, 1, 0, 2, 2)
|
||||
|
||||
if runningCollectors is not None:
|
||||
for runningCollectorCategory in runningCollectors:
|
||||
for runningCollector in runningCollectors[runningCollectorCategory][collector]:
|
||||
for value in runningCollectors.values():
|
||||
for runningCollector in value[collector]:
|
||||
newTreeItem = QtWidgets.QTreeWidgetItem(newCollectorInstanceTree)
|
||||
newTreeItem.setText(0, runningCollector['uid'])
|
||||
stopButton = QtWidgets.QPushButton("Stop")
|
||||
@@ -1683,10 +1663,10 @@ class CollectorsDialog(QtWidgets.QDialog):
|
||||
newCollector.chosenParameters)
|
||||
self.mainWindow.MESSAGEHANDLER.info('Starting server collector: ' + collector_name)
|
||||
except Exception as e:
|
||||
self.mainWindow.MESSAGEHANDLER.error('Error starting server collector: ' + str(e))
|
||||
self.mainWindow.MESSAGEHANDLER.error(f'Error starting server collector: {str(e)}')
|
||||
|
||||
def stopSelectedCollector(self, collectorToStop: str):
|
||||
self.mainWindow.MESSAGEHANDLER.info('Stopping server collector with UID: ' + collectorToStop)
|
||||
self.mainWindow.MESSAGEHANDLER.info(f'Stopping server collector with UID: {collectorToStop}')
|
||||
self.mainWindow.FCOM.stopServerCollector(collectorToStop)
|
||||
self.runningCollectorTreeItems[collectorToStop][1].takeTopLevelItem(
|
||||
self.runningCollectorTreeItems[collectorToStop][1].indexOfTopLevelItem(
|
||||
@@ -1890,7 +1870,7 @@ class ImportLinksFromCSVFile(QtWidgets.QDialog):
|
||||
tableFieldCheckBoxesWidget = QtWidgets.QWidget()
|
||||
tableFieldCheckBoxesWidgetLayout = QtWidgets.QHBoxLayout()
|
||||
tableFieldCheckBoxesWidget.setLayout(tableFieldCheckBoxesWidgetLayout)
|
||||
for fieldIndex in range(columnNumber):
|
||||
for _ in range(columnNumber):
|
||||
checkBoxWidget = QtWidgets.QCheckBox('Include Field? ')
|
||||
checkBoxWidget.setToolTip('Check the box to include this column as an attribute for the resolution.')
|
||||
self.fieldIncludeCheckBoxes.append(checkBoxWidget)
|
||||
@@ -1955,9 +1935,7 @@ class ImportLinksFromCSVFile(QtWidgets.QDialog):
|
||||
importLayout.addWidget(buttonsWidget)
|
||||
|
||||
def changeMappingForField(self, newIndex):
|
||||
for comboBoxIndex in range(len(self.fieldMappingComboBoxes)):
|
||||
comboBox = self.fieldMappingComboBoxes[comboBoxIndex]
|
||||
checkBox = self.fieldIncludeCheckBoxes[comboBoxIndex]
|
||||
for comboBox, checkBox in zip(self.fieldMappingComboBoxes, self.fieldIncludeCheckBoxes):
|
||||
if comboBox.currentIndex() == newIndex:
|
||||
if newIndex == 0:
|
||||
checkBox.setEnabled(True)
|
||||
@@ -1967,9 +1945,8 @@ class ImportLinksFromCSVFile(QtWidgets.QDialog):
|
||||
checkBox.setChecked(True)
|
||||
else:
|
||||
comboBox.setCurrentIndex(0)
|
||||
if newIndex != 0:
|
||||
checkBox.setEnabled(True)
|
||||
checkBox.setChecked(False)
|
||||
checkBox.setEnabled(True)
|
||||
checkBox.setChecked(False)
|
||||
|
||||
def checkIfEntitiesAreMapped(self):
|
||||
# Check if Entity One and Entity Two labels are assigned, do not proceed if not.
|
||||
@@ -2037,7 +2014,7 @@ class ImportLinkEntitiesFromCSVFile(QtWidgets.QDialog):
|
||||
tableFieldAttributeMapping = QtWidgets.QWidget()
|
||||
tableFieldAttributeMappingLayout = QtWidgets.QHBoxLayout()
|
||||
tableFieldAttributeMapping.setLayout(tableFieldAttributeMappingLayout)
|
||||
for fieldIndex in range(columnNumber):
|
||||
for _ in range(columnNumber):
|
||||
fieldMappingWidget = QtWidgets.QComboBox()
|
||||
fieldMappingWidget.setEditable(False)
|
||||
fieldMappingWidget.currentIndexChanged.connect(self.changeMappingForField)
|
||||
@@ -2090,12 +2067,7 @@ class ImportLinkEntitiesFromCSVFile(QtWidgets.QDialog):
|
||||
def confirmThatPrimaryFieldIsMapped(self):
|
||||
primaryField = self.parent().parent().RESOURCEHANDLER.getPrimaryFieldForEntityType(
|
||||
self.entityTypeChoiceDropdown.currentText())
|
||||
primaryFieldMapped = False
|
||||
for comboBox in self.fieldMappingComboBoxes:
|
||||
if comboBox.currentText() == primaryField:
|
||||
# Just doing self.accept() here will not work.
|
||||
primaryFieldMapped = True
|
||||
break
|
||||
primaryFieldMapped = any(comboBox.currentText() == primaryField for comboBox in self.fieldMappingComboBoxes)
|
||||
if primaryFieldMapped:
|
||||
self.accept()
|
||||
else:
|
||||
@@ -2143,7 +2115,7 @@ class ImportEntityFromCSVFile(QtWidgets.QDialog):
|
||||
tableFieldAttributeMapping = QtWidgets.QWidget()
|
||||
tableFieldAttributeMappingLayout = QtWidgets.QHBoxLayout()
|
||||
tableFieldAttributeMapping.setLayout(tableFieldAttributeMappingLayout)
|
||||
for fieldIndex in range(columnNumber):
|
||||
for _ in range(columnNumber):
|
||||
fieldMappingWidget = QtWidgets.QComboBox()
|
||||
fieldMappingWidget.setEditable(False)
|
||||
fieldMappingWidget.currentIndexChanged.connect(self.changeMappingForField)
|
||||
@@ -2321,10 +2293,10 @@ class ImportFromTextFileDialog(QtWidgets.QDialog):
|
||||
textTable = QtWidgets.QTableWidget(3, 1, self)
|
||||
textTable.setWordWrap(True)
|
||||
textTable.setFixedWidth(450)
|
||||
for line in range(len(fileContents)):
|
||||
columnItem = QtWidgets.QTableWidgetItem(fileContents[line])
|
||||
for lineIndex, lineValue in enumerate(fileContents):
|
||||
columnItem = QtWidgets.QTableWidgetItem(lineValue)
|
||||
columnItem.setFlags(columnItem.flags() & ~QtCore.Qt.ItemIsEditable)
|
||||
textTable.setItem(line, 0, columnItem)
|
||||
textTable.setItem(lineIndex, 0, columnItem)
|
||||
textTable.setColumnWidth(0, 450)
|
||||
textTable.setHorizontalHeaderLabels(['File Entities Preview'])
|
||||
|
||||
@@ -2561,8 +2533,7 @@ class ScreenshotWebsiteThread(QtCore.QThread):
|
||||
)
|
||||
urlPath = Path(os.environ['APPDATA']) / 'Mozilla' / 'Firefox' / 'Profiles'
|
||||
|
||||
tabsFilePath = list(urlPath.glob('*default*/sessionstore-backups/recovery.jsonlz4'))
|
||||
if len(tabsFilePath) != 0:
|
||||
if tabsFilePath := list(urlPath.glob('*default*/sessionstore-backups/recovery.jsonlz4')):
|
||||
tabsFilePath = tabsFilePath[0]
|
||||
cookiesDatabasePath = tabsFilePath.parent.parent / 'cookies.sqlite'
|
||||
browserCookies = self.menuBar.firefoxCookiesHelper(cookiesDatabasePath)
|
||||
@@ -2623,7 +2594,7 @@ class SaveWebsiteThread(QtCore.QThread):
|
||||
responseURL = response.url
|
||||
responseURLFragments = responseURL.split('/')[3:]
|
||||
savePath = currTempDir
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
if response.ok:
|
||||
for fragment in responseURLFragments[:-1]:
|
||||
savePath /= fragment
|
||||
@@ -2632,12 +2603,10 @@ class SaveWebsiteThread(QtCore.QThread):
|
||||
filename = responseURLFragments[-1]
|
||||
except IndexError:
|
||||
filename = ''
|
||||
if filename == '':
|
||||
if not filename:
|
||||
filename = 'index.html'
|
||||
with open(savePath / filename, "wb") as fileToWrite:
|
||||
fileToWrite.write(response.body())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.firefox.launch()
|
||||
@@ -2655,8 +2624,7 @@ class SaveWebsiteThread(QtCore.QThread):
|
||||
)
|
||||
urlPath = Path(os.environ['APPDATA']) / 'Mozilla' / 'Firefox' / 'Profiles'
|
||||
|
||||
tabsFilePath = list(urlPath.glob('*default*/sessionstore-backups/recovery.jsonlz4'))
|
||||
if len(tabsFilePath) != 0:
|
||||
if tabsFilePath := list(urlPath.glob('*default*/sessionstore-backups/recovery.jsonlz4')):
|
||||
tabsFilePath = tabsFilePath[0]
|
||||
cookiesDatabasePath = tabsFilePath.parent.parent / 'cookies.sqlite'
|
||||
browserCookies = self.menuBar.firefoxCookiesHelper(cookiesDatabasePath)
|
||||
@@ -2673,13 +2641,11 @@ class SaveWebsiteThread(QtCore.QThread):
|
||||
archiveDir = baseFilesPath / (tldextract.extract(website).fqdn + ' Snapshot ' + str(time.time_ns()))
|
||||
|
||||
for _ in range(3):
|
||||
try:
|
||||
with contextlib.suppress(TimeoutError):
|
||||
page.goto(website)
|
||||
page.keyboard.press("End")
|
||||
page.wait_for_load_state("networkidle")
|
||||
break
|
||||
except TimeoutError:
|
||||
pass
|
||||
progressValue += 1
|
||||
self.progressSignal.emit(progressValue)
|
||||
|
||||
@@ -2734,11 +2700,7 @@ class ImportBrowserTabsThread(QtCore.QThread):
|
||||
)
|
||||
urlPath = Path(os.environ['APPDATA']) / 'Mozilla' / 'Firefox' / 'Profiles'
|
||||
|
||||
tabsFilePath = list(urlPath.glob('*default*/sessionstore-backups/recovery.jsonlz4'))
|
||||
if len(tabsFilePath) == 0:
|
||||
self.mainWindow.warningSignalListener.emit('No Firefox session detected. Skipping importing '
|
||||
'from Firefox.', True)
|
||||
else:
|
||||
if tabsFilePath := list(urlPath.glob('*default*/sessionstore-backups/recovery.jsonlz4')):
|
||||
tabsFilePath = tabsFilePath[0]
|
||||
if recordSession:
|
||||
tabsToOpen = []
|
||||
@@ -2837,23 +2799,20 @@ class ImportBrowserTabsThread(QtCore.QThread):
|
||||
if urlSaveDir is not None:
|
||||
timeNow = str(datetime.now().timestamp() * 1000000).split('.')[0]
|
||||
screenshotSavePath = str(urlSaveDir / (
|
||||
actualURL.replace('/', '+') +
|
||||
' ' + timeNow + ' screenshot.png'))
|
||||
actualURL.replace('/', '+') + ' ' + timeNow + ' screenshot.png'))
|
||||
screenshotSavePath = screenshotSavePath.replace('\\', '+')
|
||||
|
||||
screenshotEntity = {'Image Name': decodedPath + ' Screenshot ' +
|
||||
timeNow,
|
||||
screenshotEntity = {'Image Name': f"{decodedPath} Screenshot {timeNow}",
|
||||
'File Path': screenshotSavePath,
|
||||
'Entity Type': 'Image'}
|
||||
|
||||
try:
|
||||
page.screenshot(path=screenshotSavePath, full_page=True)
|
||||
except Error:
|
||||
try:
|
||||
page.screenshot(path=screenshotSavePath, full_page=False)
|
||||
except Error:
|
||||
screenshotEntity = {'Phrase': 'Could not take screenshot of ' +
|
||||
decodedPath,
|
||||
screenshotEntity = {'Phrase': f'Could not take screenshot of '
|
||||
f'{decodedPath}',
|
||||
'Entity Type': 'Phrase'}
|
||||
|
||||
returnResults.append(
|
||||
@@ -2866,10 +2825,12 @@ class ImportBrowserTabsThread(QtCore.QThread):
|
||||
newEntity.append({historyMark: {'Resolution': 'Next Page'}})
|
||||
historyMark = len(returnResults)
|
||||
returnResults.append(newEntity)
|
||||
|
||||
else:
|
||||
self.mainWindow.warningSignalListener.emit('No Firefox session detected. Skipping importing '
|
||||
'from Firefox.', True)
|
||||
browser.close()
|
||||
except Error as e:
|
||||
self.mainWindow.warningSignalListener.emit('Cannot import tabs from Firefox: ' + str(repr(e)), True)
|
||||
self.mainWindow.warningSignalListener.emit(f'Cannot import tabs from Firefox: {str(repr(e))}', True)
|
||||
|
||||
if self.importDialog.chromeChoice.isChecked() and not self.cancelled:
|
||||
progressValue = 2
|
||||
@@ -3017,7 +2978,7 @@ class ImportBrowserTabsThread(QtCore.QThread):
|
||||
page.screenshot(path=screenshotSavePath, full_page=True)
|
||||
|
||||
returnResults.append(
|
||||
[{'Image Name': decodedPath + ' Screenshot ' + timeNow,
|
||||
[{'Image Name': f"{decodedPath} Screenshot {timeNow}",
|
||||
'File Path': screenshotSavePath,
|
||||
'Entity Type': 'Image'},
|
||||
{len(returnResults) - 1: {'Resolution': 'Screenshot of Tab',
|
||||
|
||||
225
Core/LQL.py
225
Core/LQL.py
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import contextlib
|
||||
from typing import Union, Optional, Any
|
||||
from uuid import uuid4
|
||||
import re
|
||||
@@ -27,10 +28,9 @@ class LQLQueryBuilder:
|
||||
self.mainWindow = mainWindow
|
||||
|
||||
def takeSnapshot(self):
|
||||
self.mainWindow.LENTDB.dbLock.acquire()
|
||||
# Create a copy
|
||||
self.databaseSnapshot = self.mainWindow.LENTDB.database.copy()
|
||||
self.mainWindow.LENTDB.dbLock.release()
|
||||
with self.mainWindow.LENTDB.dbLock:
|
||||
# Create a copy
|
||||
self.databaseSnapshot = self.mainWindow.LENTDB.database.copy()
|
||||
|
||||
self.databaseEntities = set(self.databaseSnapshot.nodes)
|
||||
|
||||
@@ -82,16 +82,14 @@ class LQLQueryBuilder:
|
||||
|
||||
def parseSelect(self, selectClause: str, selectValue: Union[str, list]):
|
||||
if selectClause == 'SELECT':
|
||||
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 set([entityField for entityField in selectValue if entityField in self.allEntityFields])
|
||||
else:
|
||||
try:
|
||||
clauseValue = re.compile(selectValue)
|
||||
return set([entityField for entityField in self.allEntityFields if clauseValue.match(entityField)])
|
||||
except re.error:
|
||||
return set()
|
||||
return self.allEntityFields if '*' in selectValue else \
|
||||
{entityField for entityField in selectValue if entityField in self.allEntityFields}
|
||||
|
||||
try:
|
||||
clauseValue = re.compile(selectValue)
|
||||
return {entityField for entityField in self.allEntityFields if clauseValue.match(entityField)}
|
||||
except re.error:
|
||||
return set()
|
||||
|
||||
def parseSource(self, sourceClause: str, sourceValues: Union[None, list], fieldsToSelect: set) -> set:
|
||||
"""
|
||||
@@ -118,32 +116,24 @@ class LQLQueryBuilder:
|
||||
except (ValueError, re.error):
|
||||
continue
|
||||
|
||||
# Not the most efficient way of phrasing this, but by far the most compact and legible.
|
||||
for matchingCanvas in matchingCanvases:
|
||||
if sourceValue[0] == 'AND':
|
||||
if sourceValue[2] is True:
|
||||
resultEntitySet = self.canvasAndNot(resultEntitySet,
|
||||
self.canvasesEntitiesDict[matchingCanvas])
|
||||
else:
|
||||
resultEntitySet = self.canvasAnd(resultEntitySet,
|
||||
self.canvasesEntitiesDict[matchingCanvas])
|
||||
resultEntitySet = self.canvasAndNot(resultEntitySet, self.canvasesEntitiesDict[matchingCanvas])\
|
||||
if sourceValue[2] is True else\
|
||||
self.canvasAnd(resultEntitySet, self.canvasesEntitiesDict[matchingCanvas])
|
||||
|
||||
elif sourceValue[2] is True:
|
||||
resultEntitySet = self.canvasOrNot(resultEntitySet,
|
||||
self.canvasesEntitiesDict[matchingCanvas],
|
||||
self.databaseEntities)
|
||||
else:
|
||||
# If this is the first clause, or'ing the empty initial resultEntitySet is what we want.
|
||||
if sourceValue[2] is True:
|
||||
resultEntitySet = self.canvasOrNot(resultEntitySet,
|
||||
self.canvasesEntitiesDict[matchingCanvas],
|
||||
self.databaseEntities)
|
||||
else:
|
||||
resultEntitySet = self.canvasOr(resultEntitySet,
|
||||
self.canvasesEntitiesDict[matchingCanvas])
|
||||
resultEntitySet = self.canvasOr(resultEntitySet,
|
||||
self.canvasesEntitiesDict[matchingCanvas])
|
||||
|
||||
# Filter out all entities that do not contain at least one of the selected fields.
|
||||
for entity in list(resultEntitySet):
|
||||
validEntity = False
|
||||
for field in fieldsToSelect:
|
||||
if field in self.allEntities[entity].keys():
|
||||
validEntity = True
|
||||
break
|
||||
validEntity = any(field in self.allEntities[entity].keys() for field in fieldsToSelect)
|
||||
|
||||
if not validEntity:
|
||||
resultEntitySet.remove(entity)
|
||||
self.allEntities.pop(entity)
|
||||
@@ -190,30 +180,26 @@ class LQLQueryBuilder:
|
||||
attributeRegex = re.compile(userInput1)
|
||||
except re.error:
|
||||
continue
|
||||
for field in self.allEntityFields:
|
||||
if attributeRegex.match(field):
|
||||
matchingFields.append(field)
|
||||
matchingFields.extend(field for field in self.allEntityFields if attributeRegex.match(field))
|
||||
|
||||
for matchingField in matchingFields:
|
||||
entitiesToRemove = []
|
||||
for entity in self.allEntities:
|
||||
attributeKeyValue = str(self.allEntities[entity].get(matchingField))
|
||||
if not self.checkVCHelper(conditionValue[2], isNot, attributeKeyValue, userInput2):
|
||||
if conditionClause[0] == "AND":
|
||||
entitiesToRemove.append(entity)
|
||||
else:
|
||||
if self.checkVCHelper(conditionValue[2], isNot, attributeKeyValue, userInput2):
|
||||
uidsToSelect.add(entity)
|
||||
elif conditionClause[0] == "AND":
|
||||
entitiesToRemove.append(entity)
|
||||
for entityToRemove in entitiesToRemove:
|
||||
uidsToSelect.remove(entityToRemove)
|
||||
|
||||
elif conditionClause[1] == "Graph Condition":
|
||||
entitiesToRemove = []
|
||||
for entity in self.allEntities:
|
||||
if not self.checkGCHelper(firstArgument, isNot, [entity] + conditionValue[1:]):
|
||||
if conditionClause[0] == "AND":
|
||||
entitiesToRemove.append(entity)
|
||||
else:
|
||||
if self.checkGCHelper(firstArgument, isNot, [entity] + conditionValue[1:]):
|
||||
uidsToSelect.add(entity)
|
||||
elif conditionClause[0] == "AND":
|
||||
entitiesToRemove.append(entity)
|
||||
for entityToRemove in entitiesToRemove:
|
||||
uidsToSelect.remove(entityToRemove)
|
||||
|
||||
@@ -236,169 +222,119 @@ class LQLQueryBuilder:
|
||||
return canvasSetA.union(allEntitiesSet.difference(canvasSetB))
|
||||
|
||||
def checkEQ(self, valueA: str, valueB: str):
|
||||
if valueA == valueB:
|
||||
return True
|
||||
return False
|
||||
return valueA == valueB
|
||||
|
||||
def checkContains(self, valueA: str, valueB: str):
|
||||
if valueB in valueA:
|
||||
return True
|
||||
return False
|
||||
return valueB in valueA
|
||||
|
||||
def checkStartsWith(self, valueA: str, valueB: str):
|
||||
if valueA.startswith(valueB):
|
||||
return True
|
||||
return False
|
||||
return valueA.startswith(valueB)
|
||||
|
||||
def checkEndsWith(self, valueA: str, valueB: str):
|
||||
if valueA.endswith(valueB):
|
||||
return True
|
||||
return False
|
||||
return valueA.endswith(valueB)
|
||||
|
||||
def checkRMatch(self, valueA: str, valueB: str):
|
||||
try:
|
||||
with contextlib.suppress(re.error):
|
||||
valueMatch = re.compile(valueB)
|
||||
if valueMatch.match(valueA):
|
||||
return True
|
||||
except re.error:
|
||||
pass
|
||||
return False
|
||||
|
||||
def checkVCHelper(self, checkType: str, isNot: bool, valueA: str, valueB: str):
|
||||
returnVal = False
|
||||
if checkType == "EQ":
|
||||
returnVal = self.checkEQ(valueA, valueB)
|
||||
elif checkType == "CONTAINS":
|
||||
if checkType == "CONTAINS":
|
||||
returnVal = self.checkContains(valueA, valueB)
|
||||
elif checkType == "STARTSWITH":
|
||||
returnVal = self.checkStartsWith(valueA, valueB)
|
||||
elif checkType == "ENDSWITH":
|
||||
returnVal = self.checkEndsWith(valueA, valueB)
|
||||
elif checkType == "EQ":
|
||||
returnVal = self.checkEQ(valueA, valueB)
|
||||
elif checkType == "RMATCH":
|
||||
returnVal = self.checkRMatch(valueA, valueB)
|
||||
if isNot:
|
||||
return not returnVal
|
||||
return returnVal
|
||||
elif checkType == "STARTSWITH":
|
||||
returnVal = self.checkStartsWith(valueA, valueB)
|
||||
return not returnVal if isNot else returnVal
|
||||
|
||||
def checkParentOf(self, valueA: str, valueB: str):
|
||||
return self.databaseSnapshot.has_successor(valueA, valueB)
|
||||
|
||||
def checkAncestorOf(self, valueA: str, valueB: str):
|
||||
try:
|
||||
with contextlib.suppress(nx.NetworkXError):
|
||||
if valueB in nx.descendants(self.databaseSnapshot, valueA):
|
||||
return True
|
||||
except nx.NetworkXError:
|
||||
pass
|
||||
return False
|
||||
|
||||
def checkChildOf(self, valueA: str, valueB: str):
|
||||
return self.databaseSnapshot.has_predecessor(valueA, valueB)
|
||||
|
||||
def checkDescendantOf(self, valueA: str, valueB: str):
|
||||
try:
|
||||
with contextlib.suppress(nx.NetworkXError):
|
||||
if valueB in nx.ancestors(self.databaseSnapshot, valueA):
|
||||
return True
|
||||
except nx.NetworkXError:
|
||||
pass
|
||||
return False
|
||||
|
||||
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
|
||||
return (valueB == "<" and numChildren < valueC) or \
|
||||
(valueB == "<=" and numChildren <= valueC) or \
|
||||
(valueB == ">" and numChildren > valueC) or \
|
||||
(valueB == ">=" and numChildren >= valueC) or \
|
||||
(valueB == "==" and numChildren == valueC)
|
||||
|
||||
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
|
||||
return (valueB == "<" and numParents < valueC) or \
|
||||
(valueB == "<=" and numParents <= valueC) or \
|
||||
(valueB == ">" and numParents > valueC) or \
|
||||
(valueB == ">=" and numParents >= valueC) or \
|
||||
(valueB == "==" and numParents == valueC)
|
||||
|
||||
def checkConnectedTo(self, valueA: str, valueB: str):
|
||||
try:
|
||||
with contextlib.suppress(nx.NetworkXError):
|
||||
if nx.has_path(self.databaseSnapshot, valueA, valueB):
|
||||
return True
|
||||
except nx.NetworkXError:
|
||||
pass
|
||||
return False
|
||||
|
||||
def checkIsolated(self, valueA: str):
|
||||
try:
|
||||
with contextlib.suppress(nx.NetworkXError):
|
||||
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:
|
||||
with contextlib.suppress(nx.NetworkXError):
|
||||
if len(self.databaseSnapshot.in_edges(valueA)) == 0:
|
||||
return True
|
||||
except nx.NetworkXError:
|
||||
pass
|
||||
return False
|
||||
|
||||
def checkIsLeaf(self, valueA: str):
|
||||
try:
|
||||
with contextlib.suppress(nx.NetworkXError):
|
||||
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":
|
||||
if checkType == "ANCESTOROF":
|
||||
returnVal = self.checkAncestorOf(*args)
|
||||
elif checkType == "NUMCHILDREN":
|
||||
returnVal = self.checkNumChildren(*args)
|
||||
elif checkType == "NUMPARENTS":
|
||||
returnVal = self.checkNumParents(*args)
|
||||
elif checkType == "CHILDOF":
|
||||
returnVal = self.checkChildOf(*args)
|
||||
elif checkType == "CONNECTEDTO":
|
||||
returnVal = self.checkConnectedTo(*args)
|
||||
elif checkType == "DESCENDANTOF":
|
||||
returnVal = self.checkDescendantOf(*args)
|
||||
elif checkType == "ISLEAF":
|
||||
returnVal = self.checkIsLeaf(*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
|
||||
elif checkType == "NUMCHILDREN":
|
||||
returnVal = self.checkNumChildren(*args)
|
||||
elif checkType == "NUMPARENTS":
|
||||
returnVal = self.checkNumParents(*args)
|
||||
elif checkType == "PARENTOF":
|
||||
returnVal = self.checkParentOf(*args)
|
||||
return not returnVal if isNot else returnVal
|
||||
|
||||
def modifyNumify(self, valueA: str):
|
||||
# Get the first number that shows up.
|
||||
@@ -457,17 +393,15 @@ class LQLQueryBuilder:
|
||||
for entity in self.allEntities:
|
||||
for modifyField in modifyFields:
|
||||
entityFieldValue = self.allEntities[entity].get(modifyField)
|
||||
if entityFieldValue is None:
|
||||
if entityFieldValue is None or modificationType not in ["UPPERCASE", "LOWERCASE", "NUMIFY"]:
|
||||
newFieldValue = None
|
||||
elif modificationType == "UPPERCASE":
|
||||
newFieldValue = self.modifyUpperCase(entityFieldValue)
|
||||
elif modificationType == "LOWERCASE":
|
||||
newFieldValue = self.modifyLowerCase(entityFieldValue)
|
||||
elif modificationType == "NUMIFY":
|
||||
else:
|
||||
newFieldValue = self.modifyNumify(entityFieldValue)
|
||||
numifiedFields.add(modifyField)
|
||||
else:
|
||||
newFieldValue = None
|
||||
if newFieldValue is not None:
|
||||
modifiedUIDs.add(entity)
|
||||
self.allEntities[entity][modifyField] = newFieldValue
|
||||
@@ -476,19 +410,16 @@ class LQLQueryBuilder:
|
||||
|
||||
def parseQuery(self, selectClause: str, selectValue: Union[str, list], sourceClause: str,
|
||||
sourceValues: Union[None, list], conditionClauses: Union[None, list],
|
||||
modifyQueries: Union[list, None] = None) -> \
|
||||
Optional[tuple[Optional[tuple[set, Union[set[Any], set[Union[str, Any]]]]],
|
||||
Optional[tuple[set[Any], set[Any]]]]]:
|
||||
modifyQueries: Union[list, None] = None) -> Optional[tuple[Optional[tuple[set, Union[set[Any], set[Union[str, Any]]]]],
|
||||
Optional[tuple[set[Any], set[Any]]]]]:
|
||||
|
||||
if self.databaseSnapshot is None:
|
||||
return None
|
||||
|
||||
returnValue = None
|
||||
modifications = None
|
||||
fieldsToSelect = self.parseSelect(selectClause, selectValue)
|
||||
if fieldsToSelect:
|
||||
entitiesToConsider = self.parseSource(sourceClause, sourceValues, fieldsToSelect)
|
||||
if entitiesToConsider:
|
||||
if fieldsToSelect := self.parseSelect(selectClause, selectValue):
|
||||
if entitiesToConsider := self.parseSource(sourceClause, sourceValues, fieldsToSelect):
|
||||
if conditionClauses:
|
||||
entitiesToConsider = self.parseConditions(conditionClauses, entitiesToConsider)
|
||||
returnValue = (entitiesToConsider, fieldsToSelect)
|
||||
|
||||
@@ -63,7 +63,7 @@ class MessageHandler:
|
||||
return message
|
||||
|
||||
# Set the severity level
|
||||
def setSeverityLevel(self, level: int):
|
||||
def setSeverityLevel(self, level):
|
||||
currentLogLevel = self.linkScopeLogger.level
|
||||
try:
|
||||
level = int(level)
|
||||
|
||||
@@ -10,7 +10,7 @@ from reportlab.platypus.tableofcontents import TableOfContents
|
||||
from reportlab.lib.units import cm
|
||||
|
||||
from reportlab.pdfgen import canvas
|
||||
from reportlab.platypus import Paragraph, PageBreak, Image, Spacer, Table, LongTable, ParagraphAndImage
|
||||
from reportlab.platypus import Paragraph, PageBreak, Image, Spacer, Table, ParagraphAndImage
|
||||
from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_JUSTIFY
|
||||
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
|
||||
from reportlab.lib.pagesizes import LETTER, inch
|
||||
@@ -33,7 +33,7 @@ class MyDocTemplate(BaseDocTemplate):
|
||||
style = flowable.style.name
|
||||
if style == 'Heading1':
|
||||
self.notify('TOCEntry', (0, text, self.page))
|
||||
if style == 'Heading2':
|
||||
elif style == 'Heading2':
|
||||
self.notify('TOCEntry', (1, text, self.page))
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ class ReportBuilder(canvas.Canvas):
|
||||
canvas.Canvas.save(self)
|
||||
|
||||
def drawHeaderAndFooter(self, pageCount):
|
||||
pageCountString = "Page %s of %s" % (self._pageNumber, pageCount)
|
||||
pageCountString = f"Page {self._pageNumber} of {pageCount}"
|
||||
self.saveState()
|
||||
self.setStrokeColorRGB(0, 0, 0)
|
||||
self.setLineWidth(0.5)
|
||||
@@ -96,7 +96,7 @@ class PDFReport:
|
||||
self.elements.append(spacer)
|
||||
self.elements.append(report_subtitle)
|
||||
|
||||
spacer = Spacer(100, 200)
|
||||
spacer = Spacer(100, 425)
|
||||
self.elements.append(spacer)
|
||||
|
||||
date = datetime.now().strftime('%Y-%m-%d %H:%m:%S')
|
||||
@@ -165,14 +165,16 @@ class PDFReport:
|
||||
"""
|
||||
|
||||
text = Paragraph(notes, notesParagraph)
|
||||
if type(entityImagePath) == Drawing:
|
||||
if isinstance(entityImagePath, Drawing):
|
||||
tbl = ReportImageAndParagraph(text, entityImagePath, side='left', xpad=10, ypad=0)
|
||||
elif entityImagePath.endswith('.png') or entityImagePath.endswith('.jpg'):
|
||||
entityImage = Image(entityImagePath, kind='proportional')
|
||||
entityImage = Image(entityImagePath)
|
||||
entityImage.preserveAspectRatio = True
|
||||
entityImage.drawHeight = 2 * inch
|
||||
entityImage.drawWidth = 2 * inch
|
||||
tbl = ReportImageAndParagraph(text, entityImage, side='left', xpad=10, ypad=0)
|
||||
else:
|
||||
raise ValueError('Invalid Image Type')
|
||||
|
||||
links_subHeader = Paragraph("Entity Links", psSubHeaderText)
|
||||
|
||||
@@ -188,20 +190,20 @@ class PDFReport:
|
||||
outgoing_data = [
|
||||
['Outgoing Links'],
|
||||
['Resolution Name', 'Child Entity', 'Date Created', 'Notes']]
|
||||
index = 0
|
||||
for link in outgoingLinks:
|
||||
for index, link in enumerate(outgoingLinks):
|
||||
resolutionText = link['Resolution']
|
||||
childNodeText = outgoingNames[index]
|
||||
linkNotesText = link['Notes']
|
||||
linkName = "".join([resolutionText[counter:counter+24] + "\n"
|
||||
dateCreatedText = link['Date Created']
|
||||
linkName = "".join([resolutionText[counter:counter + 24] + "\n"
|
||||
for counter in range(0, len(resolutionText), 24)])
|
||||
childNode = "".join([childNodeText[counter:counter+24] + "\n"
|
||||
for counter in range(0, len(childNodeText), 24)])
|
||||
linkNotes = "".join([linkNotesText[counter:counter+24] + "\n"
|
||||
for counter in range(0, len(linkNotesText), 24)])
|
||||
dateCreated = link['Date Created']
|
||||
childNode = "".join([childNodeText[counter:counter + 24] + "\n"
|
||||
for counter in range(0, len(childNodeText), 24)])
|
||||
linkNotes = "".join([linkNotesText[counter:counter + 24] + "\n"
|
||||
for counter in range(0, len(linkNotesText), 24)])
|
||||
dateCreated = "".join([dateCreatedText[counter:counter + 24] + "\n"
|
||||
for counter in range(0, len(dateCreatedText), 24)])
|
||||
outgoing_data.append([linkName, childNode, dateCreated, Paragraph(linkNotes)])
|
||||
index += 1
|
||||
outgoing_table = Table(data=outgoing_data, style=links_table_style, hAlign="CENTER",
|
||||
colWidths=[140, 140, 140, 140])
|
||||
spacer = Spacer(10, 10)
|
||||
@@ -218,20 +220,20 @@ class PDFReport:
|
||||
incoming_data = [
|
||||
['Incoming Links'],
|
||||
['Resolution Name', 'Parent Entity', 'Date Created', 'Notes']]
|
||||
index = 0
|
||||
for link in incomingLinks:
|
||||
for index, link in enumerate(incomingLinks):
|
||||
resolutionText = link['Resolution']
|
||||
parentNodeText = incomingNames[index]
|
||||
linkNotesText = link['Notes']
|
||||
linkName = "".join([resolutionText[counter:counter+24] + "\n"
|
||||
dateCreatedText = link['Date Created']
|
||||
linkName = "".join([resolutionText[counter:counter + 24] + "\n"
|
||||
for counter in range(0, len(resolutionText), 24)])
|
||||
parentNode = "".join([parentNodeText[counter:counter+24] + "\n"
|
||||
parentNode = "".join([parentNodeText[counter:counter + 24] + "\n"
|
||||
for counter in range(0, len(parentNodeText), 24)])
|
||||
linkNotes = "".join([linkNotesText[counter:counter+24] + "\n"
|
||||
for counter in range(0, len(linkNotesText), 24)])
|
||||
dateCreated = link['Date Created']
|
||||
linkNotes = "".join([linkNotesText[counter:counter + 24] + "\n"
|
||||
for counter in range(0, len(linkNotesText), 24)])
|
||||
dateCreated = "".join([dateCreatedText[counter:counter + 24] + "\n"
|
||||
for counter in range(0, len(dateCreatedText), 24)])
|
||||
incoming_data.append([linkName, parentNode, dateCreated, Paragraph(linkNotes)])
|
||||
index += 1
|
||||
incoming_table = Table(data=incoming_data, style=links_table_style, hAlign="CENTER",
|
||||
colWidths=[140, 140, 140, 140])
|
||||
spacer = Spacer(10, 10)
|
||||
@@ -260,7 +262,7 @@ class PDFReport:
|
||||
for key in list(entity):
|
||||
if key not in avoid_parsing_fields and key != 'Notes':
|
||||
valueText = entity[key]
|
||||
value = "".join([valueText[counter:counter+80] + "\n"
|
||||
value = "".join([valueText[counter:counter + 80] + "\n"
|
||||
for counter in range(0, len(valueText), 80)])
|
||||
entity_data.append([Paragraph(key, tableParagraph), Paragraph(value, tableParagraph)])
|
||||
elif key == 'Notes':
|
||||
@@ -268,28 +270,6 @@ class PDFReport:
|
||||
entity_notes = Paragraph(entity[key], notesParagraph)
|
||||
entity_table = Table(data=entity_data, style=entity_table_style, hAlign="CENTER")
|
||||
|
||||
appendix_header = Paragraph(f"Appendix {appendixNumber}", notesHeader)
|
||||
text = []
|
||||
images = []
|
||||
imangeNParagraph = []
|
||||
for appendixDict in appendixDicts:
|
||||
if appendixDict['AppendixEntityImage'] == '':
|
||||
text.append(Paragraph(appendixDict['AppendixEntityNotes'], notesParagraph))
|
||||
elif appendixDict['AppendixEntityNotes'] == '' and appendixDict['AppendixEntityImage'] != '':
|
||||
img = Image(Path(appendixDict['AppendixEntityImage']), kind='proportional')
|
||||
# img.preserveAspectRatio=True
|
||||
img.drawHeight = 2 * inch
|
||||
img.drawWidth = 2 * inch
|
||||
img.hAlign = 'LEFT'
|
||||
images.append(img)
|
||||
elif appendixDict['AppendixEntityNotes'] != '' and appendixDict['AppendixEntityImage'] != '':
|
||||
paragraph = appendixDict['AppendixEntityNotes']
|
||||
img = Image(Path(appendixDict['AppendixEntityImage']), kind='proportional')
|
||||
img.drawHeight = 2 * inch
|
||||
img.drawWidth = 2 * inch
|
||||
imangeNParagraph.append(
|
||||
ReportImageAndParagraph(Paragraph(paragraph), img, side='left', xpad=10, ypad=0))
|
||||
|
||||
self.elements.append(entityTitle)
|
||||
spacer = Spacer(20, 20)
|
||||
self.elements.append(spacer)
|
||||
@@ -314,11 +294,11 @@ class PDFReport:
|
||||
pie = Pie()
|
||||
pie.x = 150
|
||||
pie.y = 65
|
||||
pie.data = [int(len(incomingLinks)), int(len(outgoingLinks))]
|
||||
pie.data = [len(incomingLinks), len(outgoingLinks)]
|
||||
pie.sideLabels = 1
|
||||
pie.labels = ['Incoming: ' + str(len(incomingLinks)), 'Outgoing: ' + str(len(outgoingLinks))]
|
||||
pie.slices.strokeWidth = 1
|
||||
if int(len(incomingLinks)) > int(len(outgoingLinks)):
|
||||
if len(incomingLinks) > len(outgoingLinks):
|
||||
pie.slices[0].popout = 5
|
||||
else:
|
||||
pie.slices[1].popout = 5
|
||||
@@ -333,17 +313,29 @@ class PDFReport:
|
||||
self.elements.append(entity_notes_header)
|
||||
self.elements.append(entity_notes)
|
||||
self.elements.append(spacer)
|
||||
self.elements.append(appendix_header)
|
||||
|
||||
spacer = Spacer(20, 20)
|
||||
self.elements.append(spacer)
|
||||
for elementText in text:
|
||||
self.elements.append(elementText)
|
||||
for appendixIndex, appendixDict in enumerate(appendixDicts):
|
||||
appendix_header = Paragraph(f"Entity Appendix {appendixIndex}", notesHeader)
|
||||
self.elements.append(appendix_header)
|
||||
self.elements.append(spacer)
|
||||
for elementImage in images:
|
||||
self.elements.append(elementImage)
|
||||
self.elements.append(spacer)
|
||||
for elementBoth in imangeNParagraph:
|
||||
self.elements.append(elementBoth)
|
||||
|
||||
appendixImage = appendixDict['AppendixEntityImage']
|
||||
appendixNotes = appendixDict['AppendixEntityNotes']
|
||||
if appendixImage == '':
|
||||
self.elements.append(Paragraph(appendixNotes, notesParagraph))
|
||||
elif appendixNotes == '' and appendixImage != '':
|
||||
img = Image(Path(appendixImage))
|
||||
# img.preserveAspectRatio = True
|
||||
img.drawHeight = 3 * inch
|
||||
img.drawWidth = 3 * inch
|
||||
self.elements.append(img)
|
||||
elif appendixNotes != '' and appendixImage != '':
|
||||
paragraph = appendixNotes
|
||||
img = Image(Path(appendixImage))
|
||||
img.drawHeight = 3 * inch
|
||||
img.drawWidth = 3 * inch
|
||||
self.elements.append(ReportImageAndParagraph(Paragraph(paragraph), img, side='left', xpad=10, ypad=0))
|
||||
self.elements.append(spacer)
|
||||
|
||||
self.elements.append(PageBreak())
|
||||
@@ -356,7 +348,7 @@ class PDFReport:
|
||||
ParagraphStyle('Report', fontSize=9, justifyBreaks=1, alignment=TA_LEFT,
|
||||
justifyLastLine=1)
|
||||
|
||||
img = Image(timeLineImage, kind='proportional')
|
||||
img = Image(timeLineImage)
|
||||
img.drawHeight = 1.3 * inch
|
||||
img.drawWidth = 6 * inch
|
||||
img.hAlign = 'LEFT'
|
||||
@@ -429,11 +421,15 @@ class PDFReport:
|
||||
|
||||
head = f'Entity Report: {entityPrimaryField[i - 3]}'
|
||||
self.nextPagesHeader(True, head)
|
||||
self.entityPage(title=entityPrimaryField[i - 3], userNotes=entityListData[i][0].get('EntityNotes'),
|
||||
self.entityPage(title=entityPrimaryField[i - 3],
|
||||
userNotes=entityListData[i][0].get('EntityNotes'),
|
||||
entityImagePath=imagePath,
|
||||
appendixDicts=entityListData[i][1], outgoingLinks=outgoingLinks[i - 3],
|
||||
appendixDicts=entityListData[i][1],
|
||||
outgoingLinks=outgoingLinks[i - 3],
|
||||
incomingLinks=incomingLinks[i - 3],
|
||||
entity=entity[i - 3], incomingNames=incomingNames[i - 3], outgoingNames=outgoingNames[i - 3],
|
||||
entity=entity[i - 3],
|
||||
incomingNames=incomingNames[i - 3],
|
||||
outgoingNames=outgoingNames[i - 3],
|
||||
appendixNumber=i - 3)
|
||||
|
||||
# Graph report stuff disabled, at least for now.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import contextlib
|
||||
import importlib.util
|
||||
import sys
|
||||
from os import listdir
|
||||
@@ -40,12 +41,10 @@ class ResolutionManager:
|
||||
originTypes = resClassInst.originTypes
|
||||
resultTypes = resClassInst.resultTypes
|
||||
resolutionParameters = resClassInst.parameters
|
||||
try:
|
||||
with contextlib.suppress(AttributeError):
|
||||
resolutionCategory = resClassInst.category
|
||||
if not isinstance(resolutionCategory, str):
|
||||
raise AttributeError()
|
||||
except AttributeError:
|
||||
pass
|
||||
if self.resolutions.get(resolutionCategory) is None:
|
||||
self.resolutions[resolutionCategory] = {}
|
||||
self.resolutions[resolutionCategory][resNameString] = {'name': resNameString,
|
||||
@@ -56,9 +55,9 @@ class ResolutionManager:
|
||||
'category': resolutionCategory,
|
||||
'resolution': resClass
|
||||
}
|
||||
self.messageHandler.info("Loaded Resolution: " + resNameString)
|
||||
self.messageHandler.info(f"Loaded Resolution: {resNameString}")
|
||||
except Exception as e:
|
||||
self.messageHandler.error("Cannot load resolutions from " + str(directory) + "\n Info: " + repr(e))
|
||||
self.messageHandler.error(f"Cannot load resolutions from {str(directory)}" + "\n Info: " + repr(e))
|
||||
exceptionsCount += 1
|
||||
if exceptionsCount > 3:
|
||||
# Will not occur when loading modules with 3 or fewer resolutions, but that should be fine.
|
||||
@@ -68,30 +67,24 @@ class ResolutionManager:
|
||||
def getResolutionParameters(self, resolutionCategory, resolutionNameString):
|
||||
resolutionsList = self.resolutions.get(resolutionCategory)
|
||||
if resolutionsList is not None and resolutionNameString in resolutionsList:
|
||||
parameters = self.resolutions[resolutionCategory][resolutionNameString]['parameters']
|
||||
return parameters
|
||||
return self.resolutions[resolutionCategory][resolutionNameString]['parameters']
|
||||
return None
|
||||
|
||||
def getResolutionOriginTypes(self, resolutionCategoryNameString: str) -> Union[list, None]:
|
||||
resolutionCategory, resolutionName = resolutionCategoryNameString.split('/', 1)
|
||||
try:
|
||||
with contextlib.suppress(TypeError):
|
||||
if resolutionName in self.resolutions.get(resolutionCategory):
|
||||
originTypes = self.resolutions[resolutionCategory][resolutionName]['originTypes']
|
||||
if '*' in originTypes:
|
||||
originTypes = self.mainWindow.RESOURCEHANDLER.getAllEntities()
|
||||
return originTypes
|
||||
except TypeError:
|
||||
pass
|
||||
return None
|
||||
|
||||
def getResolutionDescription(self, resolutionCategoryNameString: str) -> Union[str, None]:
|
||||
resolutionCategory, resolutionName = resolutionCategoryNameString.split('/', 1)
|
||||
try:
|
||||
with contextlib.suppress(TypeError):
|
||||
if resolutionName in self.resolutions.get(resolutionCategory):
|
||||
resolutionDescription = self.resolutions[resolutionCategory][resolutionName].get('description', '')
|
||||
return resolutionDescription
|
||||
except TypeError:
|
||||
pass
|
||||
return self.resolutions[resolutionCategory][resolutionName].get('description', '')
|
||||
return None
|
||||
|
||||
def loadResolutionsFromServer(self, serverRes) -> None:
|
||||
@@ -130,9 +123,7 @@ class ResolutionManager:
|
||||
return result
|
||||
|
||||
def getResolutionsInCategory(self, category) -> list:
|
||||
if category in self.resolutions:
|
||||
return list(self.resolutions[category])
|
||||
return []
|
||||
return list(self.resolutions[category]) if category in self.resolutions else []
|
||||
|
||||
def getAllResolutions(self) -> list:
|
||||
categories = self.getResolutionCategories()
|
||||
@@ -144,7 +135,7 @@ class ResolutionManager:
|
||||
def executeResolution(self, resolutionCategoryNameString: str, resolutionEntitiesInput: list, parameters: dict,
|
||||
resolutionUID: str):
|
||||
resolutionCategory, resolutionName = resolutionCategoryNameString.split('/', 1)
|
||||
try:
|
||||
with contextlib.suppress(TypeError):
|
||||
if resolutionName in self.resolutions.get(resolutionCategory):
|
||||
if self.resolutions[resolutionCategory][resolutionName].get('resolution') == '':
|
||||
# If resolution class does not exist locally, then assume it exists on the server.
|
||||
@@ -153,10 +144,7 @@ class ResolutionManager:
|
||||
# Returning a bool, so we know that the resolution is running on the server.
|
||||
return True
|
||||
resolutionClass = self.resolutions[resolutionCategory][resolutionName]['resolution']()
|
||||
result = resolutionClass.resolution(resolutionEntitiesInput, parameters)
|
||||
return result
|
||||
except TypeError:
|
||||
pass
|
||||
return resolutionClass.resolution(resolutionEntitiesInput, parameters)
|
||||
return None
|
||||
|
||||
def createMacro(self, resolutionList: list) -> str:
|
||||
@@ -198,7 +186,7 @@ class ResolutionManager:
|
||||
|
||||
def save(self):
|
||||
macroFilePath = self.getMacroFilePath()
|
||||
macroFilePathTmp = macroFilePath.with_suffix(macroFilePath.suffix + '.tmp')
|
||||
macroFilePathTmp = macroFilePath.with_suffix(f'{macroFilePath.suffix}.tmp')
|
||||
|
||||
with open(macroFilePathTmp, "wb") as macroFile:
|
||||
dump(self.macros, macroFile)
|
||||
|
||||
@@ -61,13 +61,12 @@ class ContainsPhrase:
|
||||
counter += 1
|
||||
|
||||
if counter > 0:
|
||||
returnResults.append([{'Phrase': primaryField + ' Contains Phrase: "' + searchPhrase +
|
||||
f'" {counter} times',
|
||||
returnResults.append([{'Phrase': f'{primaryField} Contains Phrase: "{searchPhrase}" {counter} times',
|
||||
'Entity Type': 'Phrase',
|
||||
'Notes': f'"{searchPhrase}" was found {counter} time(s)\n'
|
||||
f'Offsets: Matches at character indices: '
|
||||
f'{(", ".join(map(str, offsets)))}'},
|
||||
{uid: {'Resolution': 'Contains Phrase ' + searchPhrase,
|
||||
{uid: {'Resolution': f'Contains Phrase {searchPhrase}',
|
||||
'Notes': ''}}])
|
||||
|
||||
return returnResults
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
import contextlib
|
||||
from typing import Union
|
||||
|
||||
import networkx as nx
|
||||
import re
|
||||
from datetime import timezone
|
||||
from defusedxml.ElementTree import parse
|
||||
from datetime import datetime
|
||||
from os import listdir
|
||||
@@ -89,10 +91,7 @@ class ResourceHandler:
|
||||
self.loadCoreEntities()
|
||||
|
||||
def getEntityCategories(self) -> list:
|
||||
eList = []
|
||||
for category in self.entityCategoryList:
|
||||
eList.append(category)
|
||||
return eList
|
||||
return list(self.entityCategoryList)
|
||||
|
||||
def getAllEntityDetailsWithIconsInCategory(self, category) -> list:
|
||||
eList = []
|
||||
@@ -108,8 +107,7 @@ class ResourceHandler:
|
||||
try:
|
||||
for category in self.entityCategoryList:
|
||||
if entityType in self.entityCategoryList[category]:
|
||||
for attribute in self.entityCategoryList[category][entityType]['Attributes']:
|
||||
aList.append(attribute)
|
||||
aList.extend(iter(self.entityCategoryList[category][entityType]['Attributes']))
|
||||
break
|
||||
except KeyError:
|
||||
self.messageHandler.error("Attempted to get attributes for "
|
||||
@@ -121,16 +119,12 @@ class ResourceHandler:
|
||||
"""
|
||||
Get all Entity Types in the specified category.
|
||||
"""
|
||||
eList = []
|
||||
for entity in self.entityCategoryList[category]:
|
||||
eList.append(entity)
|
||||
return eList
|
||||
return list(self.entityCategoryList[category])
|
||||
|
||||
def getCategoryOfEntityType(self, entityType: Union[str, None]):
|
||||
for category in self.entityCategoryList:
|
||||
if entityType in self.entityCategoryList[category]:
|
||||
return category
|
||||
return None
|
||||
return next((category for category in self.entityCategoryList
|
||||
if entityType in self.entityCategoryList[category]),
|
||||
None)
|
||||
|
||||
def getAllEntities(self) -> list:
|
||||
"""
|
||||
@@ -138,8 +132,7 @@ class ResourceHandler:
|
||||
"""
|
||||
eList = []
|
||||
for category in self.getEntityCategories():
|
||||
for entity in self.getAllEntitiesInCategory(category):
|
||||
eList.append(entity)
|
||||
eList.extend(iter(self.getAllEntitiesInCategory(category)))
|
||||
return eList
|
||||
|
||||
def validateAttributesOfEntity(self, entityJSON: dict) -> (bool, str):
|
||||
@@ -156,7 +149,7 @@ class ResourceHandler:
|
||||
if attrValue is None or not self.runCheckOnAttribute(
|
||||
attrValue,
|
||||
self.entityCategoryList[entityCategory][entityType]['Attributes'][attribute][1]):
|
||||
return 'Bad value: ' + str(attrValue)
|
||||
return f'Bad value: {str(attrValue)}'
|
||||
except Exception:
|
||||
return False
|
||||
return True
|
||||
@@ -172,9 +165,7 @@ class ResourceHandler:
|
||||
if attrCheck is None:
|
||||
return False
|
||||
result = attrCheck.findall(attribute)
|
||||
if len(result) == 1:
|
||||
return True
|
||||
return False
|
||||
return len(result) == 1
|
||||
|
||||
def addRecognisedEntityTypes(self, entityFile: Path) -> bool:
|
||||
try:
|
||||
@@ -191,32 +182,28 @@ class ResourceHandler:
|
||||
try:
|
||||
entityName = entity.tag.replace('_', ' ')
|
||||
attributes = entity.find('Attributes')
|
||||
attributesDict = {}
|
||||
primaryCount = 0
|
||||
attributesDict = {}
|
||||
for attribute in list(attributes):
|
||||
attributeName = attribute.text
|
||||
defaultValue = attribute.attrib['default']
|
||||
valueCheck = attribute.attrib['check']
|
||||
isPrimary = True if attribute.attrib['primary'] == 'True' else False
|
||||
isPrimary = attribute.attrib['primary'] == 'True'
|
||||
if isPrimary:
|
||||
if primaryCount > 0:
|
||||
raise AttributeError('Malformed Entity: ' + entityName + ' - too many primary fields')
|
||||
raise AttributeError(f'Malformed Entity: {entityName} - too many primary fields')
|
||||
else:
|
||||
primaryCount += 1
|
||||
if self.runCheckOnAttribute(defaultValue, valueCheck):
|
||||
attributesDict[attributeName] = [attribute.attrib['default'], attribute.attrib['check'],
|
||||
isPrimary]
|
||||
else:
|
||||
raise AttributeError('Malformed Entity: ' + entityName + ' - default values do not conform to '
|
||||
'their corresponding checks.')
|
||||
if not self.runCheckOnAttribute(defaultValue, valueCheck):
|
||||
raise AttributeError(f'Malformed Entity: {entityName} - default values do not pass their '
|
||||
f'corresponding checks.')
|
||||
attributeName = attribute.text
|
||||
attributesDict[attributeName] = [attribute.attrib['default'], attribute.attrib['check'], isPrimary]
|
||||
if primaryCount != 1:
|
||||
raise AttributeError('Malformed Entity: ' + entityName + ' - invalid number of primary fields '
|
||||
'specified.')
|
||||
raise AttributeError(f'Malformed Entity: {entityName} - invalid number of primary fields '
|
||||
f'specified.')
|
||||
|
||||
icon = entity.find('Icon')
|
||||
if icon is not None:
|
||||
icon = icon.text.strip()
|
||||
elif icon is None or icon == '':
|
||||
icon = 'Default.svg'
|
||||
icon = icon.text.strip() if icon is not None else 'Default.svg'
|
||||
if self.entityCategoryList.get(category) is None:
|
||||
self.entityCategoryList[category] = {}
|
||||
self.entityCategoryList[category][entityName] = {
|
||||
@@ -224,7 +211,7 @@ class ResourceHandler:
|
||||
'Icon': str(Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) / "Resources" / "Icons" / icon)}
|
||||
except (KeyError, AttributeError) as err:
|
||||
# Ignore malformed entities
|
||||
self.messageHandler.error('Error: ' + str(err), popUp=False)
|
||||
self.messageHandler.error(f'Error: {str(err)}', popUp=False)
|
||||
continue
|
||||
return True
|
||||
|
||||
@@ -253,8 +240,7 @@ class ResourceHandler:
|
||||
eJson[attribute] = self.entityCategoryList[category][entityType]['Attributes'][attribute][0]
|
||||
break
|
||||
except KeyError:
|
||||
self.messageHandler.error("Attempted to get attributes for "
|
||||
"malformed entity type: " + str(entityType), True)
|
||||
self.messageHandler.error(f"Attempted to get attributes for malformed entity type: {entityType}", True)
|
||||
return None
|
||||
eJson['Entity Type'] = entityType
|
||||
eJson['Date Created'] = None
|
||||
@@ -269,7 +255,7 @@ class ResourceHandler:
|
||||
if value is not None and value != '':
|
||||
eJson[key] = value
|
||||
|
||||
utcNow = datetime.isoformat(datetime.utcnow())
|
||||
utcNow = datetime.isoformat(datetime.now(timezone.utc))
|
||||
if eJson['Date Created'] is None:
|
||||
eJson['Date Created'] = utcNow
|
||||
else:
|
||||
@@ -291,8 +277,8 @@ class ResourceHandler:
|
||||
if self.entityCategoryList[category][entityType]['Attributes'][attribute][2]:
|
||||
return attribute
|
||||
except KeyError:
|
||||
self.messageHandler.error("Attempted to get primary attribute for "
|
||||
"malformed entity type: " + str(entityType), True)
|
||||
self.messageHandler.error(f"Attempted to get primary attribute for malformed entity type: {entityType}",
|
||||
True)
|
||||
return None
|
||||
|
||||
def getBareBonesEntityJson(self, entityType: str) -> Union[dict, None]:
|
||||
@@ -304,8 +290,7 @@ class ResourceHandler:
|
||||
eJson[attribute] = self.entityCategoryList[category][entityType]['Attributes'][attribute][0]
|
||||
break
|
||||
except KeyError:
|
||||
self.messageHandler.error("Attempted to get attributes for "
|
||||
"malformed entity type: " + str(entityType), True)
|
||||
self.messageHandler.error(f"Attempted to get attributes for malformed entity type: {entityType}", True)
|
||||
return None
|
||||
eJson['Entity Type'] = entityType
|
||||
|
||||
@@ -318,7 +303,7 @@ class ResourceHandler:
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
utcNow = datetime.isoformat(datetime.utcnow())
|
||||
utcNow = datetime.isoformat(datetime.now(timezone.utc))
|
||||
linkJson['Resolution'] = str(jsonData.get('Resolution')) # This way, if it is None, it is cast to a string.
|
||||
linkJson['Date Created'] = jsonData.get('Date Created')
|
||||
# Make sure that dates are always in ISO format.
|
||||
@@ -334,8 +319,8 @@ class ResourceHandler:
|
||||
|
||||
# Transfer all values from jsonData to linkJson, but preserve the values and order of linkJson for existing
|
||||
# keys.
|
||||
jsonData.update(linkJson)
|
||||
linkJson.update(jsonData)
|
||||
jsonData |= linkJson
|
||||
linkJson |= jsonData
|
||||
|
||||
return linkJson
|
||||
|
||||
@@ -354,8 +339,7 @@ class ResourceHandler:
|
||||
finally:
|
||||
with open(picture, 'rb') as pictureFile:
|
||||
pictureContents = pictureFile.read()
|
||||
pictureByteArray = QByteArray(pictureContents)
|
||||
return pictureByteArray
|
||||
return QByteArray(pictureContents)
|
||||
|
||||
def getLinkPicture(self):
|
||||
picture = Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) / "Resources" / "Icons" / "Resolution.png"
|
||||
@@ -370,11 +354,8 @@ class ResourceHandler:
|
||||
for nodeKey in graph.nodes:
|
||||
# Dereference the original dict, so we don't actually convert its icon to data.
|
||||
nodes[nodeKey] = dict(graph.nodes.get(nodeKey))
|
||||
try:
|
||||
with contextlib.suppress(KeyError):
|
||||
nodes[nodeKey]['Icon'] = nodes[nodeKey]['Icon'].toBase64().data()
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
edges = {edgeKey: graph.edges.get(edgeKey) for edgeKey in graph.edges}
|
||||
return nodes, edges
|
||||
|
||||
@@ -383,22 +364,16 @@ class ResourceHandler:
|
||||
for nodeKey in graph.nodes:
|
||||
# Dereference the original dict, so we don't actually convert its icon to data.
|
||||
nodes[nodeKey] = dict(graph.nodes.get(nodeKey))
|
||||
try:
|
||||
with contextlib.suppress(KeyError):
|
||||
nodes[nodeKey]['Icon'] = nodes[nodeKey]['Icon'].toBase64().data()
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
edges = {str(edgeKey): graph.edges.get(edgeKey) for edgeKey in graph.edges}
|
||||
return nodes, edges
|
||||
|
||||
def reconstructGraphFromString(self, graphString: str) -> tuple:
|
||||
nodes, edges = literal_eval(graphString)
|
||||
for node in nodes:
|
||||
try:
|
||||
with contextlib.suppress(KeyError):
|
||||
nodes[node]['Icon'] = QByteArray(b64decode(nodes[node]['Icon']))
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
return nodes, edges
|
||||
|
||||
def reconstructGraphFullFromFile(self, graphNodesAndEdges: Union[tuple, list]) -> nx.DiGraph:
|
||||
@@ -406,10 +381,8 @@ class ResourceHandler:
|
||||
graphNodes = graphNodesAndEdges[0]
|
||||
graphEdges = graphNodesAndEdges[1]
|
||||
for node in graphNodes:
|
||||
try:
|
||||
with contextlib.suppress(KeyError):
|
||||
graphNodes[node]['Icon'] = QByteArray(b64decode(graphNodes[node]['Icon']))
|
||||
except KeyError:
|
||||
pass
|
||||
returnGraph.add_node(node, **graphNodes[node])
|
||||
|
||||
for edge in graphEdges:
|
||||
@@ -476,11 +449,7 @@ class SingleChoicePropertyInput(QtWidgets.QGroupBox):
|
||||
vboxLayout.addWidget(radioButton)
|
||||
|
||||
def getValue(self):
|
||||
for option in self.options:
|
||||
if option.isChecked():
|
||||
return option.text()
|
||||
|
||||
return ''
|
||||
return next((option.text() for option in self.options if option.isChecked()), '')
|
||||
|
||||
|
||||
class MultiChoicePropertyInput(QtWidgets.QGroupBox):
|
||||
@@ -507,12 +476,7 @@ class MultiChoicePropertyInput(QtWidgets.QGroupBox):
|
||||
vboxLayout.addWidget(checkBox)
|
||||
|
||||
def getValue(self):
|
||||
valuesSelected = []
|
||||
for option in self.options:
|
||||
if option.isChecked():
|
||||
valuesSelected.append(option.text())
|
||||
|
||||
return valuesSelected
|
||||
return [option.text() for option in self.options if option.isChecked()]
|
||||
|
||||
|
||||
class MinSizeStackedLayout(QtWidgets.QStackedLayout):
|
||||
@@ -550,12 +514,11 @@ class RichNotesEditor(QtWidgets.QTextBrowser):
|
||||
self.textFormat = self.currentCharFormat()
|
||||
|
||||
def startEditing(self) -> None:
|
||||
if self.allowEditing:
|
||||
if self.isReadOnly():
|
||||
# Reset char format to plain text.
|
||||
self.setCurrentCharFormat(self.textFormat)
|
||||
self.setPlainText(self.contents)
|
||||
self.setReadOnly(False)
|
||||
if self.allowEditing and self.isReadOnly():
|
||||
# Reset char format to plain text.
|
||||
self.setCurrentCharFormat(self.textFormat)
|
||||
self.setPlainText(self.contents)
|
||||
self.setReadOnly(False)
|
||||
|
||||
def stopEditing(self) -> None:
|
||||
if not self.isReadOnly():
|
||||
@@ -573,15 +536,13 @@ class RichNotesEditor(QtWidgets.QTextBrowser):
|
||||
|
||||
def mousePressEvent(self, ev: QtGui.QMouseEvent) -> None:
|
||||
potentialLink = self.anchorAt(ev.pos())
|
||||
if not potentialLink:
|
||||
if ev.button() == QtGui.Qt.LeftButton:
|
||||
self.startEditing()
|
||||
if not potentialLink and ev.button() == QtGui.Qt.LeftButton:
|
||||
self.startEditing()
|
||||
super(RichNotesEditor, self).mousePressEvent(ev)
|
||||
|
||||
def focusOutEvent(self, ev: QtGui.QFocusEvent) -> None:
|
||||
if not self.underMouse():
|
||||
if self.isActiveWindow():
|
||||
self.stopEditing()
|
||||
if not self.underMouse() and self.isActiveWindow():
|
||||
self.stopEditing()
|
||||
super(RichNotesEditor, self).focusOutEvent(ev)
|
||||
|
||||
def doSetSource(self, name: Union[QUrl, str], resourceType: QtGui.QTextDocument.ResourceType = ...) -> None:
|
||||
|
||||
@@ -63,10 +63,9 @@ class SettingsObject(dict):
|
||||
# Save and then move to prevent corruption if the application closes unexpectedly.
|
||||
actualSavePath = str(Path(self.value("Project/BaseDir")).joinpath(self.value("Project/Name") + ".linkscope"))
|
||||
if is_path_exists_or_creatable_portable(actualSavePath):
|
||||
tempSavePath = actualSavePath + '.tmp'
|
||||
projectFile = open(tempSavePath, "wb")
|
||||
dump(self, projectFile)
|
||||
projectFile.close()
|
||||
tempSavePath = f'{actualSavePath}.tmp'
|
||||
with open(tempSavePath, "wb") as projectFile:
|
||||
dump(self, projectFile)
|
||||
move(tempSavePath, actualSavePath)
|
||||
|
||||
def load(self, savedDict: dict):
|
||||
|
||||
@@ -31,11 +31,7 @@ class URLManager:
|
||||
Takes a list of QUrls and returns a list of entities that correspond
|
||||
to them.
|
||||
"""
|
||||
returnValue = []
|
||||
for url in urls:
|
||||
returnValue.append(self.handleURL(url))
|
||||
|
||||
return returnValue
|
||||
return [self.handleURL(url) for url in urls]
|
||||
|
||||
def handleURL(self, url):
|
||||
parsedURL = urlparse(url.toString())
|
||||
@@ -66,10 +62,8 @@ class URLManager:
|
||||
fileType = magic.from_file(urlPathString, mime=True).split('/')[0]
|
||||
if fileType == "video":
|
||||
entityJson = {"Video Name": urlName, "File Path": savePathString, "Entity Type": "Video"}
|
||||
pass
|
||||
elif fileType == "image":
|
||||
entityJson = {"Image Name": urlName, "File Path": savePathString, "Entity Type": "Image"}
|
||||
pass
|
||||
else:
|
||||
entityJson = {"Document Name": urlName, "File Path": savePathString, "Entity Type": "Document"}
|
||||
return entityJson
|
||||
@@ -85,12 +79,12 @@ class URLManager:
|
||||
savePath = valuePath.relative_to(projectFilesPath)
|
||||
except ValueError:
|
||||
# The file selected is not in Project Files
|
||||
createSymlink = True if self.mainWindow.SETTINGS.value("Project/Symlink or Copy Materials") == "Symlink" \
|
||||
else False
|
||||
createSymlink = self.mainWindow.SETTINGS.value("Project/Symlink or Copy Materials") == "Symlink"
|
||||
|
||||
projectFilesPath = Path(self.mainWindow.SETTINGS.value("Project/FilesDir"))
|
||||
# Create a unique path in Project Files
|
||||
saveHash = hexlify(sha3_512(str(urlPath).encode()).digest()).decode()[:16] # nosec
|
||||
savePath = projectFilesPath / (saveHash + '|' + urlPath.name)
|
||||
savePath = projectFilesPath / f'{saveHash}|{urlPath.name}'
|
||||
|
||||
if createSymlink:
|
||||
symlink(urlPath, savePath)
|
||||
@@ -103,8 +97,6 @@ class URLManager:
|
||||
|
||||
def handleRemoteURL(self, url):
|
||||
stringURL = url.toString()
|
||||
if self.mainWindow.RESOURCEHANDLER.runCheckOnAttribute(stringURL, 'Onion'):
|
||||
entity = {'Entity Type': 'Onion Website', 'Onion URL': stringURL}
|
||||
else:
|
||||
entity = {'Entity Type': 'Website', 'URL': stringURL}
|
||||
return entity
|
||||
return {'Entity Type': 'Onion Website', 'Onion URL': stringURL} \
|
||||
if self.mainWindow.RESOURCEHANDLER.runCheckOnAttribute(stringURL, 'Onion') \
|
||||
else {'Entity Type': 'Website', 'URL': stringURL}
|
||||
|
||||
@@ -747,16 +747,11 @@ class InstallWizard(QtWidgets.QWizard):
|
||||
if self.currentId() == 3:
|
||||
return 6
|
||||
if self.currentId() == 4:
|
||||
if self.currentOS == 'Windows' and not self.graphvizExists:
|
||||
return 2
|
||||
else:
|
||||
return 5
|
||||
return 2 if self.currentOS == 'Windows' and not self.graphvizExists else 5
|
||||
if self.currentId() == 5:
|
||||
return 6
|
||||
if self.currentId() == 6:
|
||||
return -1
|
||||
# If we lose the user somehow, return them to the last page.
|
||||
return 6
|
||||
return -1 if self.currentId() == 6 else 6
|
||||
|
||||
def __init__(self):
|
||||
super(InstallWizard, self).__init__()
|
||||
@@ -775,11 +770,10 @@ class InstallWizard(QtWidgets.QWizard):
|
||||
try:
|
||||
if ctypes.windll.shell32.IsUserAnAdmin() == 0:
|
||||
raise ValueError('Not an admin')
|
||||
else:
|
||||
QtWidgets.QMessageBox.critical(self, 'Elevated Privileges Detected',
|
||||
'The Installer must be ran as a normal user, not as an '
|
||||
'Administrator. Please run the Installer normally.')
|
||||
sys.exit(-2)
|
||||
QtWidgets.QMessageBox.critical(self, 'Elevated Privileges Detected',
|
||||
'The Installer must be ran as a normal user, not as an '
|
||||
'Administrator. Please run the Installer normally.')
|
||||
sys.exit(-2)
|
||||
|
||||
except Exception:
|
||||
self.desktopShortcutPath = Path.home() / 'Desktop' / 'LinkScope.lnk'
|
||||
@@ -790,7 +784,7 @@ class InstallWizard(QtWidgets.QWizard):
|
||||
for textPart in releasesParts:
|
||||
if 'Windows10-x64.7z' in textPart:
|
||||
urlPart = textPart.split('"')[1].strip()
|
||||
self.downloadURL = 'https://github.com' + urlPart
|
||||
self.downloadURL = f'https://github.com{urlPart}'
|
||||
break
|
||||
|
||||
newArgs = ['"' + str(self.desktopShortcutPath) + '"', str(self.graphvizExists),
|
||||
@@ -815,7 +809,7 @@ class InstallWizard(QtWidgets.QWizard):
|
||||
for textPart in releasesParts:
|
||||
if 'Ubuntu-x64.7z' in textPart:
|
||||
urlPart = textPart.split('"')[1].strip()
|
||||
self.downloadURL = 'https://github.com' + urlPart
|
||||
self.downloadURL = f'https://github.com{urlPart}'
|
||||
break
|
||||
|
||||
# No need to wrap these in quotes
|
||||
@@ -841,10 +835,6 @@ class InstallWizard(QtWidgets.QWizard):
|
||||
subprocess.run(
|
||||
['dbus-launch', 'gio', 'set', str(self.desktopShortcutPath), "metadata::trusted",
|
||||
'true'])
|
||||
# subprocess.run(
|
||||
# ["dbus-send --type=method_call --dest=org.gnome.Shell /org/gnome/Shell "
|
||||
# "org.gnome.Shell.Eval string:'global.reexec_self()'"],
|
||||
# shell=True)
|
||||
sys.exit(0)
|
||||
else:
|
||||
sys.exit(-1)
|
||||
@@ -855,7 +845,7 @@ class InstallWizard(QtWidgets.QWizard):
|
||||
sys.exit(-5)
|
||||
else:
|
||||
self.desktopShortcutPath = Path(sys.argv[1])
|
||||
self.graphvizExists = True if sys.argv[2] == 'True' else False
|
||||
self.graphvizExists = sys.argv[2] == 'True'
|
||||
self.baseSoftwarePath = Path(sys.argv[3])
|
||||
self.executablePath = Path(sys.argv[4])
|
||||
self.downloadURL = sys.argv[5]
|
||||
@@ -910,18 +900,19 @@ class InstallWizard(QtWidgets.QWizard):
|
||||
self.show()
|
||||
|
||||
def removeFileHelper(self, pathToRemove: Path):
|
||||
if pathToRemove.exists():
|
||||
pathToRemove.chmod(0o777)
|
||||
if pathToRemove.is_dir():
|
||||
for dirpath, dirnames, filenames in os.walk(pathToRemove):
|
||||
Path(dirpath).chmod(0o777)
|
||||
filenames.extend(dirnames)
|
||||
for filename in filenames:
|
||||
filePath = Path(dirpath) / filename
|
||||
filePath.chmod(0o777)
|
||||
shutil.rmtree(pathToRemove)
|
||||
else:
|
||||
pathToRemove.unlink(missing_ok=True)
|
||||
if not pathToRemove.exists():
|
||||
return
|
||||
pathToRemove.chmod(0o777)
|
||||
if pathToRemove.is_dir():
|
||||
for dirpath, dirnames, filenames in os.walk(pathToRemove):
|
||||
Path(dirpath).chmod(0o777)
|
||||
filenames.extend(dirnames)
|
||||
for filename in filenames:
|
||||
filePath = Path(dirpath) / filename
|
||||
filePath.chmod(0o777)
|
||||
shutil.rmtree(pathToRemove)
|
||||
else:
|
||||
pathToRemove.unlink(missing_ok=True)
|
||||
|
||||
def createShortcut(self):
|
||||
self.removeFileHelper(self.desktopShortcutPath)
|
||||
@@ -955,11 +946,9 @@ class InstallWizard(QtWidgets.QWizard):
|
||||
def downloadGraphviz(self):
|
||||
graphVizPage = requests.get('https://graphviz.org/download/')
|
||||
graphVizParts = graphVizPage.text.split('\n')
|
||||
graphVizDownloadLink = ""
|
||||
for chunk in graphVizParts:
|
||||
if '(64-bit) EXE installer' in chunk:
|
||||
graphVizDownloadLink = chunk.split('"')[1]
|
||||
break
|
||||
graphVizDownloadLink = next((chunk.split('"')[1] for chunk in graphVizParts
|
||||
if '(64-bit) EXE installer' in chunk), "")
|
||||
|
||||
if not isinstance(graphVizDownloadLink, str) or graphVizDownloadLink == "":
|
||||
raise ValueError('Cannot install GraphViz: Failed to locate the latest version of the GraphViz installer.')
|
||||
graphVizInstallerTemp = tempfile.mkstemp()
|
||||
@@ -975,25 +964,24 @@ class InstallWizard(QtWidgets.QWizard):
|
||||
|
||||
def install(self):
|
||||
# Assumes we have superuser privileges.
|
||||
if self.currentOS == 'Linux':
|
||||
# Redundant since we always try to update and install, but it's good coding practice.
|
||||
if not self.graphvizExists:
|
||||
# No need to check if this succeeds - if there are any issues with installation, we will throw
|
||||
# an error on the install command.
|
||||
subprocess.run(['apt', 'update'])
|
||||
command = subprocess.run(['apt', 'install', 'p7zip-full', 'libopengl0', 'graphviz', 'libmagic1', '-y'])
|
||||
if command.returncode != 0:
|
||||
raise ValueError('Installing new packages failed, cannot continue installation.')
|
||||
|
||||
self.removeFileHelper(self.appPath)
|
||||
with open(self.appPath, 'w') as desktopApplicationFile:
|
||||
desktopApplicationFile.write(LINUX_DESKTOP_FILE_ENTRY)
|
||||
# Mark desktop file as executable
|
||||
self.appPath.chmod(self.appPath.stat().st_mode | 0o111)
|
||||
elif self.currentOS == 'Windows':
|
||||
if self.currentOS != 'Linux':
|
||||
# Assume the user has installed / will install Graphviz.
|
||||
# We don't actually need to do anything here. Maybe in the future, register application in registry?
|
||||
pass
|
||||
return
|
||||
# Redundant since we always try to update and install, but it's good coding practice.
|
||||
if not self.graphvizExists:
|
||||
# No need to check if this succeeds - if there are any issues with installation, we will throw
|
||||
# an error on the install command.
|
||||
subprocess.run(['apt', 'update'])
|
||||
command = subprocess.run(['apt', 'install', 'p7zip-full', 'libopengl0', 'graphviz', 'libmagic1', '-y'])
|
||||
if command.returncode != 0:
|
||||
raise ValueError('Installing new packages failed, cannot continue installation.')
|
||||
|
||||
self.removeFileHelper(self.appPath)
|
||||
with open(self.appPath, 'w') as desktopApplicationFile:
|
||||
desktopApplicationFile.write(LINUX_DESKTOP_FILE_ENTRY)
|
||||
# Mark desktop file as executable
|
||||
self.appPath.chmod(self.appPath.stat().st_mode | 0o111)
|
||||
|
||||
|
||||
class IntroInstallUninstallPage(QtWidgets.QWizardPage):
|
||||
@@ -1077,11 +1065,10 @@ class LinkScopeInstallLatestPage(QtWidgets.QWizardPage):
|
||||
self.installLabel.setText('Installation failed, click "Commit" to proceed.')
|
||||
|
||||
def validatePage(self) -> bool:
|
||||
if self.progressBar.value() != 10:
|
||||
if not self.processStarted:
|
||||
self.doStuff()
|
||||
else:
|
||||
if self.progressBar.value() == 10:
|
||||
return True
|
||||
if not self.processStarted:
|
||||
self.doStuff()
|
||||
return False
|
||||
|
||||
def __init__(self):
|
||||
|
||||
301
LinkScope.py
301
LinkScope.py
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Load modules
|
||||
import contextlib
|
||||
import re
|
||||
import sys
|
||||
import tempfile
|
||||
@@ -11,6 +12,7 @@ import itertools
|
||||
import threading
|
||||
|
||||
import networkx as nx
|
||||
from svglib.svglib import svg2rlg
|
||||
from ast import literal_eval
|
||||
from uuid import uuid4
|
||||
from shutil import move
|
||||
@@ -66,7 +68,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
newChildren = [childUID for childUID in list(entityJSON['Child UIDs'])
|
||||
if childUID not in targetCanvas.sceneGraph.nodes]
|
||||
# Don't create the entity if all the nodes in it already exist on the target canvas.
|
||||
if len(newChildren) == 0:
|
||||
if not newChildren:
|
||||
return None
|
||||
newEntity = self.LENTDB.addEntity(
|
||||
{'Group Name': entityJSON['Group Name'] + ' Copy',
|
||||
@@ -127,7 +129,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
self.setStatus("Project Saved.", 3000)
|
||||
self.MESSAGEHANDLER.info('Project Saved')
|
||||
except Exception as e:
|
||||
errorMessage = "Could not Save Project: " + str(repr(e))
|
||||
errorMessage = f"Could not Save Project: {repr(e)}"
|
||||
self.MESSAGEHANDLER.error(errorMessage, exc_info=True)
|
||||
self.setStatus("Failed Saving Project.", 3000)
|
||||
self.MESSAGEHANDLER.info("Failed Saving Project " + self.SETTINGS.value("Project/Name", 'Untitled'))
|
||||
@@ -208,7 +210,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
|
||||
self.setWindowTitle("LinkScope - " + self.SETTINGS.get('Project/Name', 'Untitled'))
|
||||
self.saveProject()
|
||||
self.setStatus('Project Saved As: ' + newProjectPath.name)
|
||||
self.setStatus(f'Project Saved As: {newProjectPath.name}')
|
||||
|
||||
# https://networkx.org/documentation/stable/reference/readwrite/graphml.html
|
||||
def exportCanvasToGraphML(self):
|
||||
@@ -229,7 +231,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
nx.write_graphml(currentCanvasGraph, filePath)
|
||||
self.setStatus('Canvas exported successfully.')
|
||||
except Exception as exc:
|
||||
self.MESSAGEHANDLER.error("Could not export canvas to file: " + str(exc), popUp=True)
|
||||
self.MESSAGEHANDLER.error(f"Could not export canvas to file: {str(exc)}", popUp=True)
|
||||
self.setStatus('Canvas export failed.')
|
||||
|
||||
def importCanvasFromGraphML(self):
|
||||
@@ -252,7 +254,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
nodesToReadFirst = [entity for entity in nodesToReadFirst if entity is not None and
|
||||
entity['Entity Type'] == 'EntityGroup']
|
||||
for entity in nodesToReadFirst:
|
||||
# Create new group entity so we don't mess with the contents of the original.
|
||||
# Create new group entity, so we don't mess with the contents of the original.
|
||||
if entity['uid'] not in currentScene.sceneGraph.nodes:
|
||||
newGroupEntity = self.copyGroupEntity(entity['uid'], currentScene)
|
||||
if newGroupEntity is not None:
|
||||
@@ -271,14 +273,13 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
"do not exist in the database.", popUp=True)
|
||||
self.setStatus('Canvas import aborted.')
|
||||
except Exception as exc:
|
||||
self.MESSAGEHANDLER.error("Cannot import canvas: " + str(exc), popUp=True)
|
||||
self.MESSAGEHANDLER.error(f"Cannot import canvas: {str(exc)}", popUp=True)
|
||||
self.setStatus('Canvas import failed.')
|
||||
|
||||
def exportDatabaseToGraphML(self):
|
||||
# Need to create a new database to remove the icons
|
||||
self.LENTDB.dbLock.acquire()
|
||||
currentDatabase = self.LENTDB.database.copy()
|
||||
self.LENTDB.dbLock.release()
|
||||
with self.LENTDB.dbLock:
|
||||
currentDatabase = self.LENTDB.database.copy()
|
||||
|
||||
for node in currentDatabase.nodes:
|
||||
# Remove icons. Will reset custom icons to default, but saves space.
|
||||
@@ -305,7 +306,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
nx.write_graphml(currentDatabase, filePath)
|
||||
self.setStatus('Database exported successfully.')
|
||||
except Exception as exc:
|
||||
self.MESSAGEHANDLER.error("Could not export database to file: " + str(exc), popUp=True)
|
||||
self.MESSAGEHANDLER.error(f"Could not export database to file: {str(exc)}", popUp=True)
|
||||
self.setStatus('Database export failed.')
|
||||
|
||||
def importDatabaseFromGraphML(self):
|
||||
@@ -323,18 +324,22 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
read_graphml_nodes = {key: read_graphml.nodes[key] for key in read_graphml.nodes}
|
||||
read_graphml_edges = {key: read_graphml.edges[key] for key in read_graphml.edges}
|
||||
for node in read_graphml_nodes:
|
||||
read_graphml_nodes[node]['Icon'] = self.RESOURCEHANDLER.getEntityDefaultPicture(
|
||||
read_graphml_nodes[node]['Entity Type'])
|
||||
# Make sure that all the necessary values are assigned.
|
||||
# This will throw an exception if the user imports an entity without a type.
|
||||
read_graphml_nodes[node] = self.RESOURCEHANDLER.getEntityJson(
|
||||
read_graphml_nodes[node]['Entity Type'], read_graphml_nodes[node])
|
||||
if read_graphml_nodes[node].get('Child UIDs'):
|
||||
read_graphml_nodes[node]['Child UIDs'] = literal_eval(read_graphml_nodes[node]['Child UIDs'])
|
||||
for edge in read_graphml_edges:
|
||||
read_graphml_edges[edge]['uid'] = literal_eval(read_graphml_edges[edge]['uid'])
|
||||
# Make sure that all the necessary values are assigned.
|
||||
read_graphml_edges[edge] = self.RESOURCEHANDLER.getLinkJson(read_graphml_edges[edge])
|
||||
self.LENTDB.mergeDatabases(read_graphml_nodes, read_graphml_edges, fromServer=False)
|
||||
self.dockbarOne.existingEntitiesPalette.loadEntities()
|
||||
self.LENTDB.resetTimeline()
|
||||
self.setStatus('Database imported successfully.')
|
||||
except Exception as exc:
|
||||
self.MESSAGEHANDLER.error("Could not import database from file: " + str(exc), popUp=True)
|
||||
self.MESSAGEHANDLER.error(f"Could not import database from file: {str(exc)}", popUp=True)
|
||||
self.setStatus('Database import failed.')
|
||||
|
||||
def generateReport(self):
|
||||
@@ -382,26 +387,23 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
self.SETTINGS.setValue("Project/Name", newName)
|
||||
|
||||
move(oldBaseDir, self.SETTINGS.value("Project/BaseDir"))
|
||||
oldProjectFile = newBaseDir.joinpath(oldName + '.linkscope')
|
||||
oldProjectFile = newBaseDir.joinpath(f'{oldName}.linkscope')
|
||||
oldProjectFile.unlink(missing_ok=True)
|
||||
|
||||
self.setWindowTitle("LinkScope - " + self.SETTINGS.get('Project/Name', 'Untitled'))
|
||||
self.saveProject()
|
||||
statusMessage = 'Project Renamed to: ' + newName
|
||||
statusMessage = f'Project Renamed to: {newName}'
|
||||
self.setStatus(statusMessage)
|
||||
self.MESSAGEHANDLER.info(statusMessage)
|
||||
|
||||
def addCanvas(self) -> None:
|
||||
# Create or open canvas
|
||||
connected = False
|
||||
if self.FCOM.isConnected():
|
||||
connected = True
|
||||
|
||||
connected = self.FCOM.isConnected()
|
||||
with self.syncedCanvasesLock:
|
||||
availableSyncedCanvases = self.syncedCanvases
|
||||
newCanvasPopup = CreateOrOpenCanvas(self, connected, availableSyncedCanvases)
|
||||
if newCanvasPopup.exec():
|
||||
self.MESSAGEHANDLER.info("New Canvas added: " + newCanvasPopup.canvasName)
|
||||
self.MESSAGEHANDLER.info(f"New Canvas added: {newCanvasPopup.canvasName}")
|
||||
|
||||
def toggleWorldDoc(self) -> None:
|
||||
if self.centralWidget() is not None:
|
||||
@@ -430,7 +432,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
def deleteSpecificEntity(self, itemUID: str) -> None:
|
||||
self.centralWidget().tabbedPane.nodeRemoveAllHelper(itemUID)
|
||||
self.LENTDB.removeEntity(itemUID)
|
||||
self.MESSAGEHANDLER.info("Deleted node: " + itemUID)
|
||||
self.MESSAGEHANDLER.info(f"Deleted node: {itemUID}")
|
||||
|
||||
def deleteSpecificLink(self, linkUIDs: set) -> None:
|
||||
"""
|
||||
@@ -446,7 +448,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
scene.removeUIDFromLink(linkUID)
|
||||
for linkUID in linkUIDs:
|
||||
self.LENTDB.removeLink(linkUID)
|
||||
self.MESSAGEHANDLER.info("Deleted link: " + str(linkUID))
|
||||
self.MESSAGEHANDLER.info(f"Deleted link: {str(linkUID)}")
|
||||
|
||||
def setGroupAppendMode(self, enable: bool) -> None:
|
||||
if self.centralWidget().tabbedPane.getCurrentScene().linking:
|
||||
@@ -536,7 +538,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
shortestPath = None
|
||||
|
||||
if shortestPath is None:
|
||||
messagePathNotFound = 'No path found connecting the selected nodes: ' + str(endPoints)
|
||||
messagePathNotFound = f'No path found connecting the selected nodes: {endPoints}'
|
||||
self.setStatus(messagePathNotFound)
|
||||
self.MESSAGEHANDLER.info(messagePathNotFound, popUp=True)
|
||||
else:
|
||||
@@ -546,7 +548,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
if itemUID in shortestPath:
|
||||
currentScene.nodesDict[itemUID].setSelected(True)
|
||||
|
||||
linksToSelect = [(a, b) for a, b in zip(shortestPath, shortestPath[1:])]
|
||||
linksToSelect = list(zip(shortestPath, shortestPath[1:]))
|
||||
for linkItem in [link for link in currentScene.items() if isinstance(link, BaseConnector)]:
|
||||
if linkItem.uid.intersection(linksToSelect):
|
||||
linkItem.setSelected(True)
|
||||
@@ -568,17 +570,17 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
|
||||
newCyclesThread = ExtractCyclesThread(tempGraph, endPoints, canvasName)
|
||||
newCyclesThread.cyclesSignal.connect(self.extractCyclesResultHandler)
|
||||
self.MESSAGEHANDLER.info('Extracting Cycles from Canvas: ' + canvasName)
|
||||
self.MESSAGEHANDLER.info(f'Extracting Cycles from Canvas: {canvasName}')
|
||||
newCyclesThread.start()
|
||||
self.cycleExtractionThreads.append(newCyclesThread)
|
||||
|
||||
def extractCyclesResultHandler(self, results: list, canvasName: str) -> None:
|
||||
if not results:
|
||||
self.MESSAGEHANDLER.info('No Cycles in Canvas: ' + canvasName)
|
||||
self.MESSAGEHANDLER.info(f'No Cycles in Canvas: {canvasName}')
|
||||
else:
|
||||
count = 0
|
||||
while True:
|
||||
newCanvasName = canvasName + ' Cycles #' + str(count)
|
||||
newCanvasName = f'{canvasName} Cycles #{str(count)}'
|
||||
if self.centralWidget().tabbedPane.addCanvas(newCanvasName):
|
||||
break
|
||||
else:
|
||||
@@ -591,16 +593,14 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
for node in nodesToAdd:
|
||||
newCanvas.addNodeProgrammatic(node)
|
||||
|
||||
count = 1
|
||||
entitiesAlreadyInGroups = set()
|
||||
for group in groupsToMake:
|
||||
for count, group in enumerate(groupsToMake, start=1):
|
||||
newGroup = set()
|
||||
for groupEntity in group:
|
||||
if groupEntity not in entitiesAlreadyInGroups:
|
||||
newGroup.add(groupEntity)
|
||||
entitiesAlreadyInGroups.add(groupEntity)
|
||||
newCanvas.groupItemsProgrammatic(newGroup, 'Group ' + str(count))
|
||||
count += 1
|
||||
newCanvas.groupItemsProgrammatic(newGroup, f'Group {str(count)}')
|
||||
newCanvas.rearrangeGraph('circular')
|
||||
|
||||
for cycleThread in list(self.cycleExtractionThreads):
|
||||
@@ -610,8 +610,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
|
||||
def findEntityOrLinkOnCanvas(self, regex: bool = False) -> None:
|
||||
currentScene = self.centralWidget().tabbedPane.getCurrentScene()
|
||||
currentUIDs = [item.uid for item in currentScene.items() if isinstance(item, BaseNode)
|
||||
or isinstance(item, BaseConnector)]
|
||||
currentUIDs = [item.uid for item in currentScene.items() if isinstance(item, (BaseNode, BaseConnector))]
|
||||
entityPrimaryFields = {}
|
||||
for uid in currentUIDs:
|
||||
if isinstance(uid, str):
|
||||
@@ -631,9 +630,9 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
findPrompt = FindEntityOnCanvasDialog(list(entityPrimaryFields), regex)
|
||||
|
||||
if findPrompt.exec():
|
||||
uidsToSelect = []
|
||||
findText = findPrompt.findInput.text()
|
||||
if findText != "":
|
||||
uidsToSelect = []
|
||||
try:
|
||||
if regex:
|
||||
expression = re.compile(findText)
|
||||
@@ -650,7 +649,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
|
||||
currentScene.clearSelection()
|
||||
for item in [linkOrEntity for linkOrEntity in currentScene.items()
|
||||
if isinstance(linkOrEntity, BaseNode) or isinstance(linkOrEntity, BaseConnector)]:
|
||||
if isinstance(linkOrEntity, (BaseNode, BaseConnector))]:
|
||||
if str(item.uid) in uidsToSelect:
|
||||
item.setSelected(True)
|
||||
if len(uidsToSelect) == 1 and ',' not in uidsToSelect[0]:
|
||||
@@ -677,10 +676,10 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
findPrompt = FindEntityOfTypeOnCanvasDialog(entityTypesOnCanvas, regex)
|
||||
|
||||
if findPrompt.exec():
|
||||
uidsToSelect = []
|
||||
findText = findPrompt.findInput.text()
|
||||
findType = findPrompt.typeInput.currentText()
|
||||
if findText != "":
|
||||
uidsToSelect = []
|
||||
try:
|
||||
if regex:
|
||||
expression = re.compile(findText)
|
||||
@@ -771,7 +770,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
if isinstance(item, BaseNode)]
|
||||
validEntityToSplit = [entity for entity in entityToSplit
|
||||
if entity['Entity Type'] != 'EntityGroup']
|
||||
if len(validEntityToSplit) == 0:
|
||||
if not validEntityToSplit:
|
||||
self.MESSAGEHANDLER.info('No valid entities to split selected! Please choose at least one non-Meta entity.',
|
||||
popUp=True)
|
||||
return
|
||||
@@ -780,6 +779,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
popUp=True)
|
||||
return
|
||||
|
||||
# Continue only if a single entity is selected.
|
||||
entityToSplit = validEntityToSplit[0]
|
||||
entityToSplitPrimaryFieldKey = list(entityToSplit)[1]
|
||||
entityToSplitUID = entityToSplit['uid']
|
||||
@@ -792,14 +792,12 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
for newEntityWithLinks in splitDialog.splitEntitiesWithLinks:
|
||||
newEntity = {entityToSplitPrimaryFieldKey: newEntityWithLinks[0]}
|
||||
for field in entityToSplit:
|
||||
if field != 'uid' and field != entityToSplitPrimaryFieldKey:
|
||||
if field not in ['uid', entityToSplitPrimaryFieldKey]:
|
||||
newEntity[field] = entityToSplit[field]
|
||||
newEntity = self.LENTDB.addEntity(newEntity)
|
||||
|
||||
for link in newEntityWithLinks[1]:
|
||||
newLink = {}
|
||||
for field in link:
|
||||
newLink[field] = link[field]
|
||||
newLink = {field: link[field] for field in link}
|
||||
if newLink['uid'][0] == entityToSplitUID:
|
||||
newLink['uid'] = (newEntity['uid'], newLink['uid'][1])
|
||||
else:
|
||||
@@ -821,9 +819,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
|
||||
def editProjectSettings(self) -> None:
|
||||
settingsDialog = ProjectEditDialog(self.SETTINGS)
|
||||
settingsConfirm = settingsDialog.exec()
|
||||
|
||||
if settingsConfirm:
|
||||
if settingsDialog.exec():
|
||||
# Save new settings
|
||||
newSettings = settingsDialog.newSettings
|
||||
for key in newSettings:
|
||||
@@ -833,25 +829,19 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
self.SETTINGS.pop(key)
|
||||
elif newSettingValue[0] != '':
|
||||
# Do not allow blank settings.
|
||||
if key == 'Project/Resolution Result Grouping Threshold' or \
|
||||
key == 'Project/Number of Answers Returned' or \
|
||||
key == 'Project/Question Answering Retriever Value' or \
|
||||
key == 'Project/Question Answering Reader Value':
|
||||
try:
|
||||
if key in ['Project/Resolution Result Grouping Threshold', 'Project/Number of Answers Returned',
|
||||
'Project/Question Answering Retriever Value', 'Project/Question Answering Reader Value']:
|
||||
with contextlib.suppress(ValueError):
|
||||
int(newSettingValue[1])
|
||||
self.SETTINGS.setValue(key, newSettingValue[0])
|
||||
except ValueError:
|
||||
pass
|
||||
elif newSettingValue[0] == 'Copy' or newSettingValue[0] == 'Symlink':
|
||||
elif newSettingValue[0] in ['Copy', 'Symlink']:
|
||||
self.SETTINGS.setValue(key, newSettingValue[0])
|
||||
|
||||
self.saveProject()
|
||||
|
||||
def editResolutionsSettings(self) -> None:
|
||||
settingsDialog = ResolutionsEditDialog(self.SETTINGS)
|
||||
settingsConfirm = settingsDialog.exec()
|
||||
|
||||
if settingsConfirm:
|
||||
if settingsDialog.exec():
|
||||
# Save new settings
|
||||
newSettings = settingsDialog.newSettings
|
||||
for key in newSettings:
|
||||
@@ -867,9 +857,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
|
||||
def editLogSettings(self) -> None:
|
||||
settingsDialog = LoggingSettingsDialog(self.SETTINGS)
|
||||
settingsConfirm = settingsDialog.exec()
|
||||
|
||||
if settingsConfirm:
|
||||
if settingsDialog.exec():
|
||||
# Save new settings
|
||||
newSettings = settingsDialog.newSettings
|
||||
for key in newSettings:
|
||||
@@ -887,9 +875,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
|
||||
def editProgramSettings(self) -> None:
|
||||
settingsDialog = ProgramEditDialog(self.SETTINGS)
|
||||
settingsConfirm = settingsDialog.exec()
|
||||
|
||||
if settingsConfirm:
|
||||
if settingsDialog.exec():
|
||||
# Save new settings
|
||||
newSettings = settingsDialog.newSettings
|
||||
for key in newSettings:
|
||||
@@ -905,30 +891,22 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
|
||||
def changeGraphics(self) -> None:
|
||||
settingsDialog = GraphicsEditDialog(self.SETTINGS, self.RESOURCEHANDLER)
|
||||
settingsConfirm = settingsDialog.exec()
|
||||
|
||||
if settingsConfirm:
|
||||
if settingsDialog.exec():
|
||||
newSettings = settingsDialog.newSettings
|
||||
try:
|
||||
with contextlib.suppress(ValueError):
|
||||
etfVal = int(newSettings["ETF"])
|
||||
self.centralWidget().tabbedPane.entityTextFont.setPointSize(etfVal)
|
||||
self.SETTINGS.setValue("Program/Graphics/EntityTextFontSize", str(newSettings["ETF"]))
|
||||
except ValueError:
|
||||
pass
|
||||
try:
|
||||
with contextlib.suppress(ValueError):
|
||||
ltfVal = int(newSettings["LTF"])
|
||||
self.centralWidget().tabbedPane.linkTextFont.setPointSize(ltfVal)
|
||||
self.SETTINGS.setValue("Program/Graphics/LinkTextFontSize", str(newSettings["LTF"]))
|
||||
except ValueError:
|
||||
pass
|
||||
try:
|
||||
with contextlib.suppress(ValueError):
|
||||
lfVal = int(newSettings["LF"])
|
||||
self.centralWidget().tabbedPane.hideZoom = - int(lfVal)
|
||||
self.centralWidget().tabbedPane.hideZoom = -lfVal
|
||||
self.centralWidget().tabbedPane.updateCanvasHideZoom()
|
||||
self.SETTINGS.setValue("Program/Graphics/LabelFade", str(newSettings["LF"]))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
etcVal = newSettings["ETC"]
|
||||
newEtcColor = QtGui.QColor(etcVal)
|
||||
if newEtcColor.isValid():
|
||||
@@ -1072,8 +1050,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
for uid in uids:
|
||||
# Connectors give the list if edge UIDs they represent
|
||||
if isinstance(uid, set):
|
||||
for linkUID in uid:
|
||||
eJson.append(self.LENTDB.getLink(linkUID))
|
||||
eJson.extend(self.LENTDB.getLink(linkUID) for linkUID in uid)
|
||||
else:
|
||||
eJson.append(self.LENTDB.getEntity(uid))
|
||||
|
||||
@@ -1090,10 +1067,8 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
"""
|
||||
for tab in self.centralWidget().tabbedPane.canvasTabs:
|
||||
scene = self.centralWidget().tabbedPane.canvasTabs[tab].scene()
|
||||
try:
|
||||
with contextlib.suppress(KeyError):
|
||||
scene.nodesDict[uid].updateLabel(label)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def updateLinkLabelsOnCanvases(self, uid: str, label: str) -> None:
|
||||
"""
|
||||
@@ -1106,10 +1081,8 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
"""
|
||||
for tab in self.centralWidget().tabbedPane.canvasTabs:
|
||||
scene = self.centralWidget().tabbedPane.canvasTabs[tab].scene()
|
||||
try:
|
||||
with contextlib.suppress(KeyError):
|
||||
scene.linksDict[uid].updateLabel(label)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def populateEntitiesWidget(self, eJson: dict, add: bool) -> None:
|
||||
if add:
|
||||
@@ -1140,9 +1113,9 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
elif parameters[parameter].get('global') is True:
|
||||
# Extra slash in the middle to ensure that resolutions cannot overwrite these accidentally (or not),
|
||||
# since slashes are not allowed by default on Linux or Windows.
|
||||
savedParameterValue = self.SETTINGS.value('Resolutions/Global/Parameters/' + parameter)
|
||||
savedParameterValue = self.SETTINGS.value(f'Resolutions/Global/Parameters/{parameter}')
|
||||
else:
|
||||
savedParameterValue = self.SETTINGS.value('Resolutions/' + resolutionName + '/' + parameter)
|
||||
savedParameterValue = self.SETTINGS.value(f'Resolutions/{resolutionName}/{parameter}')
|
||||
if savedParameterValue is not None:
|
||||
specifiedParameterValues[parameter] = savedParameterValue
|
||||
parameters.pop(parameter)
|
||||
@@ -1275,7 +1248,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
macroValid = False
|
||||
for macroIndex, runningMacro in enumerate(self.runningMacros):
|
||||
currentResolutionForMacro = runningMacro[0]
|
||||
if '/' + resolution_name in currentResolutionForMacro[0] and \
|
||||
if resolution_name == currentResolutionForMacro[0] and \
|
||||
resolution_uid == currentResolutionForMacro[1]:
|
||||
macroValid = True
|
||||
break
|
||||
@@ -1283,21 +1256,21 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
if macroValid:
|
||||
runNext = False
|
||||
runningMacro.pop(0)
|
||||
if len(runningMacro) > 0:
|
||||
if runningMacro:
|
||||
nextResolutionForMacro = runningMacro[0]
|
||||
|
||||
acceptableOriginTypes = self.RESOLUTIONMANAGER.getResolutionOriginTypes(nextResolutionForMacro[0])
|
||||
fullEntityJsonList = [self.LENTDB.getEntity(entityUID) for entityUID in affectedEntityUIDs]
|
||||
fullEntityJsonList = [self.LENTDB.getEntity(entityUID) for entityUID in set(affectedEntityUIDs)]
|
||||
filteredEntityJsonList = [entity for entity in fullEntityJsonList
|
||||
if entity['Entity Type'] in acceptableOriginTypes]
|
||||
if len(filteredEntityJsonList) > 0:
|
||||
if filteredEntityJsonList:
|
||||
preparedResolutionArguments = [nextResolutionForMacro[0], nextResolutionForMacro[1],
|
||||
filteredEntityJsonList, nextResolutionForMacro[2]]
|
||||
runNext = True
|
||||
if not runNext:
|
||||
self.setStatus('Macro execution finished.')
|
||||
self.runningMacros.remove(runningMacro)
|
||||
if runNext:
|
||||
if macroValid and runNext:
|
||||
self.runResolution(*preparedResolutionArguments)
|
||||
|
||||
def showMacrosDialog(self) -> None:
|
||||
@@ -1869,12 +1842,11 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
for answerIndex in range(answerCount):
|
||||
answer = response[answerIndex]
|
||||
if answer['answer']:
|
||||
textAns += "Answer " + str(answerIndex + 1) + ": " + answer['answer'] + "\n\n"
|
||||
textAns += "Context: ..." + answer['context'] + "...\n\n"
|
||||
textAns += "Document Used: " + answer['doc']
|
||||
textAns += "\n\n"
|
||||
textAns += f"Answer {str(answerIndex + 1)}: {answer['answer']}\n\n" \
|
||||
f"Context: ...{answer['context']}...\n\n" \
|
||||
f"Document Used: {answer['doc']}\n\n"
|
||||
else:
|
||||
textAns += "Answer " + str(answerIndex + 1) + ": No Answer\n\n"
|
||||
textAns += f"Answer {str(answerIndex + 1)}: No Answer\n\n"
|
||||
|
||||
self.dockbarTwo.oracle.answerSection.setPlainText(textAns)
|
||||
self.setStatus("Answered Question")
|
||||
@@ -2204,7 +2176,10 @@ class InitialConfigPage(QtWidgets.QWizardPage):
|
||||
options=QtWidgets.QFileDialog.DontUseNativeDialog)
|
||||
selectedPath = selectedPath[0]
|
||||
if selectedPath != '':
|
||||
self.savePathEdit.setText(str(Path(selectedPath).absolute()))
|
||||
savePath = Path(selectedPath).absolute()
|
||||
if savePath.suffix != '.pdf':
|
||||
savePath = savePath.with_suffix(f"{savePath.suffix}.pdf")
|
||||
self.savePathEdit.setText(str(savePath))
|
||||
|
||||
def getData(self):
|
||||
data = {'SavePath': self.savePathEdit.text()}
|
||||
@@ -2276,24 +2251,22 @@ class SummaryPage(QtWidgets.QWizardPage):
|
||||
class EntityPage(QtWidgets.QWizardPage):
|
||||
def __init__(self, parent: ReportWizard):
|
||||
super(EntityPage, self).__init__(parent=parent.parent())
|
||||
self.inputAppendixImageEdit = QtWidgets.QLineEdit()
|
||||
self.appendixWidget = QtWidgets.QWidget()
|
||||
self.appendixLayout = QtWidgets.QVBoxLayout()
|
||||
|
||||
self.setTitle(self.tr(f"Entity Page Wizard"))
|
||||
self.setMinimumSize(300, 700)
|
||||
|
||||
self.entityName = parent.primaryField
|
||||
self.uidPicture = parent.uid
|
||||
self.entityUID = parent.uid
|
||||
|
||||
self.inputNotesEdit = QtWidgets.QPlainTextEdit()
|
||||
self.inputImageEdit = QtWidgets.QLineEdit()
|
||||
self.button = QtWidgets.QPushButton("Add...")
|
||||
self.addAppendixButton = QtWidgets.QPushButton("Add New Appendix Section")
|
||||
self.removeAppendixButton = QtWidgets.QPushButton("Remove Last Appendix Section")
|
||||
|
||||
self.scrolllayout = QtWidgets.QVBoxLayout()
|
||||
self.scrollwidget = QtWidgets.QWidget()
|
||||
|
||||
self.defaultpic = self.parent().LENTDB.getEntity(self.uidPicture).get('Icon')
|
||||
self.defaultpic = self.parent().LENTDB.getEntity(self.entityUID).get('Icon')
|
||||
|
||||
summaryLabel = QtWidgets.QLabel(f"Entity {self.entityName} Notes: ")
|
||||
|
||||
@@ -2305,13 +2278,8 @@ class EntityPage(QtWidgets.QWizardPage):
|
||||
imageCheckBox.setChecked(False)
|
||||
imageCheckBox.toggled.connect(pDirButton.setEnabled)
|
||||
|
||||
self.button.clicked.connect(self.addSection)
|
||||
|
||||
self.button.setDisabled(True)
|
||||
|
||||
appendixCheckBox = QtWidgets.QCheckBox('Add Appendix')
|
||||
appendixCheckBox.setChecked(False)
|
||||
appendixCheckBox.toggled.connect(self.button.setEnabled)
|
||||
self.addAppendixButton.clicked.connect(self.addSection)
|
||||
self.removeAppendixButton.clicked.connect(self.removeSection)
|
||||
|
||||
self.scrollwidget.setLayout(self.scrolllayout)
|
||||
|
||||
@@ -2328,8 +2296,9 @@ class EntityPage(QtWidgets.QWizardPage):
|
||||
hLayout.addWidget(self.inputImageEdit)
|
||||
hLayout.addWidget(pDirButton)
|
||||
|
||||
hLayout.addWidget(appendixCheckBox)
|
||||
hLayout.addWidget(self.button)
|
||||
hLayout.addItem(QtWidgets.QSpacerItem(10, 30))
|
||||
hLayout.addWidget(self.addAppendixButton)
|
||||
hLayout.addWidget(self.removeAppendixButton)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.addLayout(hLayout)
|
||||
@@ -2337,7 +2306,7 @@ class EntityPage(QtWidgets.QWizardPage):
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def editPath(self):
|
||||
def editPath(self) -> None:
|
||||
selectedPath = QtWidgets.QFileDialog().getOpenFileName(parent=self, caption='Select New Icon',
|
||||
dir=str(Path.home()),
|
||||
options=QtWidgets.QFileDialog.DontUseNativeDialog,
|
||||
@@ -2345,56 +2314,22 @@ class EntityPage(QtWidgets.QWizardPage):
|
||||
if selectedPath != '':
|
||||
self.inputImageEdit.setText(str(Path(selectedPath).absolute()))
|
||||
|
||||
def editAppendixPath(self):
|
||||
selectedPath = QtWidgets.QFileDialog().getOpenFileName(parent=self, caption='Select New Icon',
|
||||
dir=str(Path.home()),
|
||||
options=QtWidgets.QFileDialog.DontUseNativeDialog,
|
||||
filter="Image Files (*.png *.jpg)")[0]
|
||||
if selectedPath != '':
|
||||
self.inputAppendixImageEdit.setText(str(Path(selectedPath).absolute()))
|
||||
def addSection(self) -> None:
|
||||
appendixWidget = AppendixWidget()
|
||||
self.scrolllayout.addWidget(appendixWidget)
|
||||
|
||||
def addSection(self):
|
||||
appendixLabelNotes = QtWidgets.QLabel("Entity Notes: ")
|
||||
inputAppendixNotesEdit = QtWidgets.QPlainTextEdit()
|
||||
imageAppendixLabel = QtWidgets.QLabel("Image Path: ")
|
||||
appendixButton = QtWidgets.QPushButton("Select Image...")
|
||||
appendixButton.clicked.connect(self.editAppendixPath)
|
||||
self.appendixLayout.addWidget(appendixLabelNotes)
|
||||
self.appendixLayout.addWidget(inputAppendixNotesEdit)
|
||||
self.appendixLayout.addWidget(imageAppendixLabel)
|
||||
self.appendixLayout.addWidget(self.inputAppendixImageEdit)
|
||||
self.appendixLayout.addWidget(appendixButton)
|
||||
self.appendixWidget.setLayout(self.appendixLayout)
|
||||
self.scrolllayout.addWidget(self.appendixWidget)
|
||||
self.button.setDisabled(True)
|
||||
def removeSection(self) -> None:
|
||||
numChildren = self.scrolllayout.count()
|
||||
if numChildren:
|
||||
appendixItem = self.scrolllayout.takeAt(numChildren - 1)
|
||||
appendixItem.widget().deleteLater()
|
||||
|
||||
def getData(self):
|
||||
import re
|
||||
from svglib.svglib import svg2rlg
|
||||
|
||||
appendixNotes = []
|
||||
if self.inputImageEdit.text() != '':
|
||||
data = {'EntityNotes': self.inputNotesEdit.toPlainText(), 'EntityImage': self.inputImageEdit.text()}
|
||||
else:
|
||||
if 'svg' in str(self.defaultpic):
|
||||
contents = bytearray(self.defaultpic)
|
||||
widthRegex = re.compile(b' width="\d*" ')
|
||||
fileContents = ''
|
||||
for widthMatches in widthRegex.findall(self.defaultpic):
|
||||
fileContents = contents.replace(widthMatches, b' ')
|
||||
heightRegex = re.compile(b' height="\d*" ')
|
||||
for heightMatches in heightRegex.findall(self.defaultpic):
|
||||
fileContents = contents.replace(heightMatches, b' ')
|
||||
fileContents = fileContents.replace(b'<svg ', b'<svg height="150" width="150" ')
|
||||
|
||||
temp_dir = tempfile.TemporaryDirectory()
|
||||
imagePath = Path(temp_dir.name) / 'entity.svg'
|
||||
with open(imagePath, 'wb') as tempFile:
|
||||
tempFile.write(bytearray(fileContents))
|
||||
|
||||
image = svg2rlg(imagePath)
|
||||
data = {'EntityNotes': self.inputNotesEdit.toPlainText(), 'EntityImage': image}
|
||||
elif 'PNG' in str(self.defaultpic):
|
||||
if 'PNG' in str(self.defaultpic):
|
||||
temp_dir = tempfile.TemporaryDirectory()
|
||||
imagePath = Path(temp_dir.name) / 'entity.png'
|
||||
with open(imagePath, 'wb') as tempFile:
|
||||
@@ -2402,9 +2337,10 @@ class EntityPage(QtWidgets.QWizardPage):
|
||||
|
||||
data = {'EntityNotes': self.inputNotesEdit.toPlainText(), 'EntityImage': str(imagePath)}
|
||||
else:
|
||||
self.defaultpic = self.parent().RESOURCEHANDLER.getEntityDefaultPicture(
|
||||
self.parent().LENTDB.getEntity(self.uidPicture)['Entity Type'])
|
||||
# Default picture is an SVG.
|
||||
if 'svg' not in str(self.defaultpic):
|
||||
# Default picture is an SVG.
|
||||
self.defaultpic = self.parent().RESOURCEHANDLER.getEntityDefaultPicture(
|
||||
self.parent().LENTDB.getEntity(self.entityUID)['Entity Type'])
|
||||
contents = bytearray(self.defaultpic)
|
||||
widthRegex = re.compile(b' width="\d*" ')
|
||||
fileContents = ''
|
||||
@@ -2423,16 +2359,43 @@ class EntityPage(QtWidgets.QWizardPage):
|
||||
image = svg2rlg(imagePath)
|
||||
data = {'EntityNotes': self.inputNotesEdit.toPlainText(), 'EntityImage': image}
|
||||
|
||||
qPlainTextNote = self.appendixWidget.findChildren(QtWidgets.QPlainTextEdit)
|
||||
qlineEdits = self.appendixWidget.findChildren(QtWidgets.QLineEdit)
|
||||
for i in range(len(qPlainTextNote)):
|
||||
appendixDict = {'AppendixEntityNotes': qPlainTextNote[i].toPlainText(),
|
||||
'AppendixEntityImage': qlineEdits[i].text()}
|
||||
for index in range(self.scrolllayout.count()):
|
||||
childWidget = self.scrolllayout.itemAt(index).widget()
|
||||
appendixDict = {'AppendixEntityNotes': childWidget.inputAppendixNotesEdit.toPlainText(),
|
||||
'AppendixEntityImage': childWidget.inputAppendixImageEdit.text()}
|
||||
appendixNotes.append(appendixDict)
|
||||
|
||||
return data, appendixNotes
|
||||
|
||||
|
||||
class AppendixWidget(QtWidgets.QWidget):
|
||||
|
||||
def __init__(self) -> None:
|
||||
super(AppendixWidget, self).__init__()
|
||||
appendixWidgetLayout = QtWidgets.QGridLayout()
|
||||
appendixLabelNotes = QtWidgets.QLabel("Entity Notes: ")
|
||||
self.inputAppendixNotesEdit = QtWidgets.QPlainTextEdit()
|
||||
imageAppendixLabel = QtWidgets.QLabel("Image Path: ")
|
||||
appendixButton = QtWidgets.QPushButton("Select Image...")
|
||||
appendixButton.clicked.connect(self.editAppendixPath)
|
||||
self.inputAppendixImageEdit = QtWidgets.QLineEdit()
|
||||
appendixWidgetLayout.addWidget(appendixLabelNotes, 0, 0, 1, 1)
|
||||
appendixWidgetLayout.addWidget(self.inputAppendixNotesEdit, 2, 0, 4, 1)
|
||||
appendixWidgetLayout.addWidget(imageAppendixLabel, 7, 0, 1, 1)
|
||||
appendixWidgetLayout.addWidget(self.inputAppendixImageEdit, 9, 0, 1, 1)
|
||||
appendixWidgetLayout.addWidget(appendixButton, 11, 0, 1, 1)
|
||||
self.setLayout(appendixWidgetLayout)
|
||||
self.inputAppendixNotesEdit.setFixedHeight(100)
|
||||
|
||||
def editAppendixPath(self) -> None:
|
||||
selectedPath = QtWidgets.QFileDialog().getOpenFileName(parent=self, caption='Select New Icon',
|
||||
dir=str(Path.home()),
|
||||
options=QtWidgets.QFileDialog.DontUseNativeDialog,
|
||||
filter="Image Files (*.png *.jpg)")[0]
|
||||
|
||||
if selectedPath != '':
|
||||
self.inputAppendixImageEdit.setText(str(Path(selectedPath).absolute()))
|
||||
|
||||
|
||||
class CreateOrOpenCanvas(QtWidgets.QDialog):
|
||||
|
||||
def __init__(self, parent, isConnectedToNetwork=False, syncedCanvases=None):
|
||||
@@ -3826,13 +3789,11 @@ class SplitEntitiesDialog(QtWidgets.QDialog):
|
||||
self.entitiesTable.setItem(newRowIndex, 0, QtWidgets.QTableWidgetItem(entityPrimaryField))
|
||||
self.entitiesTable.setFocus()
|
||||
|
||||
count = 1
|
||||
for link in self.allLinks:
|
||||
for count, link in enumerate(self.allLinks, start=1):
|
||||
selectResolution = QtWidgets.QCheckBox(link['Resolution'])
|
||||
selectResolution.setStyleSheet(Stylesheets.CHECK_BOX_STYLESHEET)
|
||||
selectResolution.linkUID = link['uid']
|
||||
self.entitiesTable.setCellWidget(newRowIndex, count, selectResolution)
|
||||
count += 1
|
||||
|
||||
def removeRow(self) -> None:
|
||||
self.entitiesTable.removeRow(self.entitiesTable.rowCount() - 1)
|
||||
@@ -4355,13 +4316,11 @@ class QueryResultsViewer(QtWidgets.QDialog):
|
||||
for index in range(1, len(self.headerFields)):
|
||||
self.resultsTable.horizontalHeader().setSectionResizeMode(index, QtWidgets.QHeaderView.Stretch)
|
||||
|
||||
count = 0
|
||||
for uid in selectedUIDs:
|
||||
for count, uid in enumerate(selectedUIDs):
|
||||
self.resultsTable.insertRow(count)
|
||||
for index, field in enumerate(self.headerFields):
|
||||
self.resultsTable.setItem(count, index, QtWidgets.QTableWidgetItem(
|
||||
str(entitiesDict[uid].get(field, 'None'))))
|
||||
count += 1
|
||||
|
||||
self.resultsTabbedPane.addTab(self.resultsTable, 'Table')
|
||||
|
||||
@@ -4728,8 +4687,8 @@ class MacroDialog(QtWidgets.QDialog):
|
||||
if 0 < len(rParameters):
|
||||
parameterSelector = ResolutionParametersSelector(
|
||||
self.mainWindowObject, resolutionName, rParameters,
|
||||
windowTitle='[' + str(itemIndex) + '/' + str(numberOfResolutionsSelected) + '] ' +
|
||||
'Select Parameter values for Resolution: ' + resolutionName)
|
||||
windowTitle=f'[{str(itemIndex + 1)}/{str(numberOfResolutionsSelected)}] Select Parameter '
|
||||
f'values for Resolution: {resolutionName}')
|
||||
if parameterSelector.exec():
|
||||
resolutionParameterValues.update(parameterSelector.chosenParameters)
|
||||
else:
|
||||
|
||||
@@ -33,14 +33,7 @@ class HaveIBeenPwnedBreachDomains:
|
||||
primaryField = entity['Domain Name']
|
||||
breachInfoRequest = requests.get(baseURL + primaryField, headers=requestHeaders)
|
||||
statusCode = breachInfoRequest.status_code
|
||||
if statusCode == 401:
|
||||
return "The HIBP API Key provided is invalid."
|
||||
elif statusCode == 429:
|
||||
sleep(2)
|
||||
continue
|
||||
elif statusCode == 503:
|
||||
return "The HIBP Service is unavailable."
|
||||
elif statusCode == 200:
|
||||
if statusCode == 200:
|
||||
breachContent = json.loads(breachInfoRequest.content)
|
||||
|
||||
for breach in breachContent:
|
||||
@@ -80,6 +73,13 @@ class HaveIBeenPwnedBreachDomains:
|
||||
'Date Created': breach['BreachDate']},
|
||||
{entity['uid']: {'Resolution': 'Contained in Breach',
|
||||
'Notes': ''}}])
|
||||
elif statusCode == 401:
|
||||
return "The HIBP API Key provided is invalid."
|
||||
elif statusCode == 429:
|
||||
sleep(2)
|
||||
continue
|
||||
elif statusCode == 503:
|
||||
return "The HIBP Service is unavailable."
|
||||
sleep(1.7)
|
||||
count += 1
|
||||
return returnResults
|
||||
|
||||
@@ -36,14 +36,7 @@ class HaveIBeenPwnedBreaches:
|
||||
breachInfoRequest = requests.get(baseURL + quote_plus(primaryField) + '?truncateResponse=false',
|
||||
headers=requestHeaders)
|
||||
statusCode = breachInfoRequest.status_code
|
||||
if statusCode == 401:
|
||||
return "The HIBP API Key provided is invalid."
|
||||
elif statusCode == 429:
|
||||
sleep(2)
|
||||
continue
|
||||
elif statusCode == 503:
|
||||
return "The HIBP Service is unavailable."
|
||||
elif statusCode == 200:
|
||||
if statusCode == 200:
|
||||
breachContent = json.loads(breachInfoRequest.content)
|
||||
|
||||
for breach in breachContent:
|
||||
@@ -83,6 +76,13 @@ class HaveIBeenPwnedBreaches:
|
||||
'Date Created': breach['BreachDate']},
|
||||
{entity['uid']: {'Resolution': 'Contained in Breach',
|
||||
'Notes': ''}}])
|
||||
elif statusCode == 401:
|
||||
return "The HIBP API Key provided is invalid."
|
||||
elif statusCode == 429:
|
||||
sleep(2)
|
||||
continue
|
||||
elif statusCode == 503:
|
||||
return "The HIBP Service is unavailable."
|
||||
sleep(1.7)
|
||||
count += 1
|
||||
return returnResults
|
||||
|
||||
@@ -34,24 +34,24 @@ class HaveIBeenPwnedPassword:
|
||||
|
||||
breachInfoRequest = requests.get(baseURL + hashPrefix, headers=requestHeaders)
|
||||
statusCode = breachInfoRequest.status_code
|
||||
if statusCode == 401:
|
||||
if statusCode == 200:
|
||||
pwnedPasswordContent = breachInfoRequest.content.decode('utf-8').split('\r\n')
|
||||
|
||||
for password in pwnedPasswordContent:
|
||||
if hashSuffix in password:
|
||||
returnResults.append([{'Phrase': f"Password Hash Found {password.split(':')[1]} times "
|
||||
f"in breach data.",
|
||||
'Entity Type': 'Phrase'},
|
||||
{entity['uid']: {'Resolution': 'Pwned Password',
|
||||
'Notes': ''}}])
|
||||
break
|
||||
elif statusCode == 401:
|
||||
return "The HIBP API Key provided is invalid."
|
||||
elif statusCode == 429:
|
||||
sleep(2)
|
||||
continue
|
||||
elif statusCode == 503:
|
||||
return "The HIBP Service is unavailable."
|
||||
elif statusCode == 200:
|
||||
pwnedPasswordContent = breachInfoRequest.content.decode('utf-8').split('\r\n')
|
||||
|
||||
for password in pwnedPasswordContent:
|
||||
if hashSuffix in password:
|
||||
returnResults.append([{'Phrase': 'Password Hash Found ' + password.split(':')[1] +
|
||||
' times in breach data.',
|
||||
'Entity Type': 'Phrase'},
|
||||
{entity['uid']: {'Resolution': 'Pwned Password',
|
||||
'Notes': ''}}])
|
||||
break
|
||||
sleep(1.7)
|
||||
count += 1
|
||||
return returnResults
|
||||
|
||||
@@ -35,24 +35,24 @@ class HaveIBeenPwnedPasswordHash:
|
||||
|
||||
breachInfoRequest = requests.get(baseURL + hashPrefix, headers=requestHeaders)
|
||||
statusCode = breachInfoRequest.status_code
|
||||
if statusCode == 401:
|
||||
if statusCode == 200:
|
||||
pwnedPasswordContent = breachInfoRequest.content.decode('utf-8').split('\r\n')
|
||||
|
||||
for password in pwnedPasswordContent:
|
||||
if hashSuffix in password:
|
||||
returnResults.append([{'Phrase': f"Password Hash Found {password.split(':')[1]} times "
|
||||
f"in breach data.",
|
||||
'Entity Type': 'Phrase'},
|
||||
{entity['uid']: {'Resolution': 'Pwned Password',
|
||||
'Notes': ''}}])
|
||||
break
|
||||
elif statusCode == 401:
|
||||
return "The HIBP API Key provided is invalid."
|
||||
elif statusCode == 429:
|
||||
sleep(2)
|
||||
continue
|
||||
elif statusCode == 503:
|
||||
return "The HIBP Service is unavailable."
|
||||
elif statusCode == 200:
|
||||
pwnedPasswordContent = breachInfoRequest.content.decode('utf-8').split('\r\n')
|
||||
|
||||
for password in pwnedPasswordContent:
|
||||
if hashSuffix in password:
|
||||
returnResults.append([{'Phrase': 'Password Hash Found ' + password.split(':')[1] +
|
||||
' times in breach data.',
|
||||
'Entity Type': 'Phrase'},
|
||||
{entity['uid']: {'Resolution': 'Pwned Password',
|
||||
'Notes': ''}}])
|
||||
break
|
||||
sleep(1.7)
|
||||
count += 1
|
||||
return returnResults
|
||||
|
||||
@@ -31,14 +31,7 @@ class HaveIBeenPwnedPastes:
|
||||
emailAddress = entity['Email Address']
|
||||
pasteInfoRequest = requests.get(baseURL + quote_plus(emailAddress), headers=requestHeaders)
|
||||
statusCode = pasteInfoRequest.status_code
|
||||
if statusCode == 401:
|
||||
return "The HIBP API Key provided is invalid."
|
||||
elif statusCode == 429:
|
||||
sleep(2)
|
||||
continue
|
||||
elif statusCode == 503:
|
||||
return "The HIBP Service is unavailable."
|
||||
elif statusCode == 200:
|
||||
if statusCode == 200:
|
||||
pasteContent = json.loads(pasteInfoRequest.content)
|
||||
|
||||
for paste in pasteContent:
|
||||
@@ -46,7 +39,7 @@ class HaveIBeenPwnedPastes:
|
||||
pasteSource = paste['Source']
|
||||
|
||||
# If Paste Date is None, then default to entity creation date.
|
||||
returnResults.append([{'Paste Identifier': pasteSource + ' | ' + pasteID,
|
||||
returnResults.append([{'Paste Identifier': f'{pasteSource} | {pasteID}',
|
||||
'Paste Title': paste['Title'],
|
||||
'Paste Source': pasteSource,
|
||||
'Paste ID': pasteID,
|
||||
@@ -55,6 +48,14 @@ class HaveIBeenPwnedPastes:
|
||||
'Date Created': paste['Date']},
|
||||
{entity['uid']: {'Resolution': 'Contained in Paste',
|
||||
'Notes': ''}}])
|
||||
|
||||
elif statusCode == 401:
|
||||
return "The HIBP API Key provided is invalid."
|
||||
elif statusCode == 429:
|
||||
sleep(2)
|
||||
continue
|
||||
elif statusCode == 503:
|
||||
return "The HIBP Service is unavailable."
|
||||
sleep(1.7)
|
||||
count += 1
|
||||
return returnResults
|
||||
|
||||
@@ -38,8 +38,8 @@ class InterpolRedNotices:
|
||||
|
||||
noticeNotes = ""
|
||||
for warrant in noticeContents['arrest_warrants']:
|
||||
noticeNotes += 'CHARGE: ' + warrant.get('charge') + '\nFROM COUNTRY: ' + \
|
||||
warrant.get('issuing_country_id') + '\n\n'
|
||||
noticeNotes += f"CHARGE: {warrant.get('charge')}\nFROM COUNTRY: " \
|
||||
f"{warrant.get('issuing_country_id')}\n\n"
|
||||
|
||||
try:
|
||||
thumbnailPictureURL = noticeContents['_links']['thumbnail']['href']
|
||||
@@ -84,7 +84,7 @@ class InterpolRedNotices:
|
||||
for entity in entityJsonList:
|
||||
uid = entity['uid']
|
||||
entityType = entity['Entity Type']
|
||||
if entityType == 'Person' or entityType == 'Politically Exposed Person':
|
||||
if entityType in ['Person', 'Politically Exposed Person']:
|
||||
primaryField = entity['Full Name'].strip()
|
||||
elif entityType == 'Phrase':
|
||||
primaryField = entity['Phrase'].strip()
|
||||
@@ -95,17 +95,13 @@ class InterpolRedNotices:
|
||||
# This ensures that we will not miss any matches.
|
||||
nameFragments = primaryField.split(' ')
|
||||
firstName = nameFragments[0].upper()
|
||||
if len(nameFragments) == 1:
|
||||
lastName = None
|
||||
else:
|
||||
lastName = nameFragments[-1].upper()
|
||||
|
||||
lastName = None if len(nameFragments) == 1 else nameFragments[-1].upper()
|
||||
firstRequestURL = firstRequestPart1 + firstName
|
||||
if lastName is not None:
|
||||
firstRequestURL += "&name=" + lastName
|
||||
firstRequestURL += f"&name={lastName}"
|
||||
firstRequestURL += firstRequestPart2
|
||||
|
||||
firstRequest = requests.get(firstRequestURL + "1" + firstRequestPart3)
|
||||
firstRequest = requests.get(f"{firstRequestURL}1{firstRequestPart3}")
|
||||
|
||||
pageContents = firstRequest.json()
|
||||
lastPage = int(pageContents['_links']['last']['href'].split('&page=')[1].split('&')[0])
|
||||
|
||||
@@ -84,7 +84,7 @@ class InterpolYellowNotices:
|
||||
for entity in entityJsonList:
|
||||
uid = entity['uid']
|
||||
entityType = entity['Entity Type']
|
||||
if entityType == 'Person' or entityType == 'Politically Exposed Person':
|
||||
if entityType in ['Person', 'Politically Exposed Person']:
|
||||
primaryField = entity['Full Name'].strip()
|
||||
elif entityType == 'Phrase':
|
||||
primaryField = entity['Phrase'].strip()
|
||||
@@ -95,17 +95,13 @@ class InterpolYellowNotices:
|
||||
# This ensures that we will not miss any matches.
|
||||
nameFragments = primaryField.split(' ')
|
||||
firstName = nameFragments[0].upper()
|
||||
if len(nameFragments) == 1:
|
||||
lastName = None
|
||||
else:
|
||||
lastName = nameFragments[-1].upper()
|
||||
|
||||
lastName = None if len(nameFragments) == 1 else nameFragments[-1].upper()
|
||||
firstRequestURL = firstRequestPart1 + firstName
|
||||
if lastName is not None:
|
||||
firstRequestURL += "&name=" + lastName
|
||||
firstRequestURL += f"&name={lastName}"
|
||||
firstRequestURL += firstRequestPart2
|
||||
|
||||
firstRequest = requests.get(firstRequestURL + "1" + firstRequestPart3)
|
||||
firstRequest = requests.get(f"{firstRequestURL}1{firstRequestPart3}")
|
||||
|
||||
pageContents = firstRequest.json()
|
||||
lastPage = int(pageContents['_links']['last']['href'].split('&page=')[1].split('&')[0])
|
||||
|
||||
Reference in New Issue
Block a user