REVAMPED FILE IMPORT.

Fixed some bugs.
This commit is contained in:
AccentuSoft
2022-01-14 22:26:00 +02:00
parent 18d076b242
commit ff24b4f482
5 changed files with 575 additions and 137 deletions

View File

@@ -126,6 +126,37 @@ class EntitiesDB:
return returnValue
def addEntities(self, entsJsonList: list, fromServer: bool = False):
self.dbLock.acquire()
returnValue = []
for entJson in entsJsonList:
# 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)
if entity is None:
continue
# Use uid as key. Code is holdover from time where 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, True)
self.mainWindow.populateEntitiesWidget(entity, add=True)
self.dbLock.release()
self.resetTimeline()
return returnValue
def addLink(self, linkJson: dict, fromServer=False):
"""
Add a link between two entities in the database.
@@ -145,10 +176,22 @@ class EntitiesDB:
"no uid to database.")
else:
linkUID = link['uid']
self.database.add_edge(linkUID[0], linkUID[1], **link)
if exists:
newRes = link.get('Resolution')
newNotes = link.get('Notes')
if newRes and newRes != exists['Resolution']:
link['Resolution'] = exists['Resolution'] + ' | ' + newRes
if newNotes and newNotes != exists['Notes']:
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)
self.dbLock.release()
if not fromServer:
self.mainWindow.sendLocalDatabaseUpdateToServer(link, True)
@@ -234,10 +277,9 @@ class EntitiesDB:
self.dbLock.release()
return returnValue
def removeEntity(self, uid: str, fromServer=False):
def removeEntity(self, uid: str, fromServer=False, updateTimeLine=True):
"""
Removes the entity with the given uid, if it
exists.
Removes the entity with the given uid, if it exists.
"""
self.dbLock.acquire()
ent = None
@@ -250,7 +292,8 @@ class EntitiesDB:
self.mainWindow.handleGroupNodeUpdateAfterEntityDeletion(uid) # Blocking - locks the db.
if not fromServer:
self.mainWindow.sendLocalDatabaseUpdateToServer(ent, False)
self.updateTimeline(ent, False)
if updateTimeLine:
self.updateTimeline(ent, False)
def removeLink(self, uid, fromServer=False):
"""
@@ -278,6 +321,23 @@ class EntitiesDB:
self.dbLock.release()
return result
def getEntityOfType(self, primaryAttr: str, entityType: str):
"""
Checks if an entity with the specified primary attribute exists, and if it does, return it.
"""
result = None
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()
return result
def getLinkIfExists(self, uid):
"""
Returns the attributes of the given link uid as a dict.
@@ -349,14 +409,14 @@ class EntitiesDB:
self.dbLock.release()
return returnValue
def isLinkNoLock(self, uid: tuple):
def isLinkNoLock(self, uid: tuple) -> Union[bool, dict]:
"""
Returns True if the uid given exists as a link, and False otherwise.
Used only in this class, as it does not lock.
"""
if self.database.edges.get(uid) is not None:
return True
return self.database.edges[uid]
return False
def getEntityType(self, uid: str):

View File

@@ -491,7 +491,7 @@ class TabbedPane(QtWidgets.QTabWidget):
self.canvasTabs[canvas].scene().addLinkProgrammatic(linkUID, lJson['Resolution'], fromServer=True)
self.canvasTabs[canvas].scene().rearrangeGraph()
def nodeRemoveAllHelper(self, nodeUID) -> None:
def nodeRemoveAllHelper(self, nodeUID: str) -> None:
for canvas in self.canvasTabs:
currentScene = self.canvasTabs[canvas].scene()
if nodeUID in currentScene.nodesDict:
@@ -679,8 +679,11 @@ class CanvasView(QtWidgets.QGraphicsView):
if isinstance(item, Entity.BaseNode):
if isinstance(item, Entity.GroupNode):
for childUID in list(item.groupedNodesUid):
self.tabbedPane.mainWindow.deleteSpecificEntity(childUID)
self.tabbedPane.mainWindow.deleteSpecificEntity(item.uid)
self.tabbedPane.nodeRemoveAllHelper(childUID)
self.tabbedPane.mainWindow.LENTDB.removeEntity(childUID, updateTimeLine=False)
self.tabbedPane.nodeRemoveAllHelper(item.uid)
self.tabbedPane.mainWindow.LENTDB.removeEntity(item.uid, updateTimeLine=False)
self.tabbedPane.mainWindow.LENTDB.resetTimeline()
def adjustSceneRect(self) -> None:
self.scene().adjustSceneRect()
@@ -1404,8 +1407,8 @@ class CanvasScene(QtWidgets.QGraphicsScene):
try:
return self.nodesDict[uid]
except KeyError:
self.parent().messageHandler.error('Canvas state is undefined: Tried to draw a link involving a '
'node not present in the canvas, uid: ' + uid)
self.parent().messageHandler.error('Canvas state is undefined: Tried to get a node not present in the '
'canvas, uid: ' + str(uid))
return None
def removeGroupNodeLinksForUID(self, groupUID, nodeUID) -> None:

View File

@@ -19,6 +19,7 @@ from typing import Union
from urllib import parse
from pathlib import Path
from datetime import datetime
from uuid import uuid4
from playwright.sync_api import sync_playwright, Error, TimeoutError
@@ -298,66 +299,158 @@ class MenuBar(QtWidgets.QMenuBar):
importDialog = ImportFromFileDialog(self)
importDialogAccept = importDialog.exec_()
return_results = []
fileDirectory = importDialog.fileDirectory
newLinks = []
fileDirectory = Path(importDialog.fileDirectoryLine.text())
if importDialogAccept and fileDirectory != '':
sceneToAddTo = None
if importDialog.importToCanvasCheckbox.isChecked():
sceneToAddTo = self.parent().centralWidget().tabbedPane.getSceneByName(
importDialog.importToCanvasDropdown.currentText())
if importDialog.CSVFileChoice.isChecked():
if fileDirectory.exists() and fileDirectory.is_file():
sceneToAddTo = None
try:
csvContents = pd.read_csv(fileDirectory, usecols=['EntityType', 'EntityField', 'EntityValue'])
for entity in range(len(csvContents['EntityType'])):
if csvContents['EntityType'][entity] in self.parent().RESOURCEHANDLER.getAllEntities():
if csvContents['EntityField'][entity] in list(
self.parent().RESOURCEHANDLER.getEntityJson(csvContents['EntityType'][entity])):
return_results.append(
[{csvContents['EntityField'][entity]: str(csvContents['EntityValue'][entity]),
'Entity Type': csvContents['EntityType'][entity]}])
else:
self.parent().MESSAGEHANDLER.warning(
'Please Check that the format of the csv provided is as described by '
f'the documentation. Malformed entity detected at line {entity + 1}. The '
f'field of the entity is Invalid', popUp=True)
else:
self.parent().MESSAGEHANDLER.warning(
"Please Check that the format of the csv provided is as described by "
f"the documentation. Malformed entity detected at line {entity + 1}. The "
f"entity type is Invalid", popUp=True)
except ValueError:
self.parent().MESSAGEHANDLER.warning("Please check that the format of the csv provided is as "
"described by the documentation. It should have 3 columns in "
"order of EntityType, EntityField, EntityValue. All the "
"'Values' should be filled accordingly", popUp=True)
return_results = []
elif importDialog.textFileChoice.isChecked():
importDialog = ImportFromTextFileDialog(self)
if importDialog.exec_():
txtFileContents = open(fileDirectory, 'r')
selectedEntityType = importDialog.importTypeDropdown.currentText()
jsonOfField = self.parent().RESOURCEHANDLER.getEntityJson(selectedEntityType)
primary_field = list(jsonOfField)[1]
for line in txtFileContents.readlines():
if re.match(r'\w', line):
return_results.append(
[{primary_field: line,
'Entity Type': selectedEntityType}])
newNodeUIDs = []
newLinks = []
for newNode in return_results:
newNodeJson = self.parent().LENTDB.addEntity(newNode[0])
if newNodeJson is not None:
newNodeUIDs.append(newNodeJson['uid'])
else:
newNodeUIDs.append(None)
if importDialog.textFileChoice.isChecked():
fileContents = []
with open(fileDirectory, 'r') as importFile:
# Read a maximum of 3 lines from the file:
count = 0
for line in importFile:
fileContents.append(line.strip())
count += 1
if count >= 3:
break
self.parent().centralWidget().tabbedPane.linkAddHelper(newLinks)
importTextFileDialog = ImportFromTextFileDialog(self, fileContents)
if importTextFileDialog.exec_():
if importTextFileDialog.importToCanvasCheckbox.isChecked():
sceneToAddTo = self.parent().centralWidget().tabbedPane.getSceneByName(
importTextFileDialog.importToCanvasDropdown.currentText())
if sceneToAddTo is not None:
for newNodeUID in newNodeUIDs:
if newNodeUID is not None:
sceneToAddTo.addNodeProgrammatic(newNodeUID)
sceneToAddTo.rearrangeGraph()
selectedEntityType = importTextFileDialog.importTypeDropdown.currentText()
primary_field = importTextFileDialog.typePrimaryFieldValueLabel.text()
with open(fileDirectory, 'r') as importFile:
for line in importFile:
lineValue = line.strip()
if lineValue:
return_results.append({primary_field: lineValue,
'Entity Type': selectedEntityType})
elif importDialog.CSVFileChoice.isChecked():
csvDF = pd.read_csv(fileDirectory)
# Remove duplicate column names
csvDF = csvDF.loc[:, ~csvDF.columns.duplicated()]
# Fill NaN values with an empty string
csvDF.fillna('')
# If we have less than 2 rows, we cannot import
rowNumber = len(csvDF.index)
if rowNumber < 2:
raise ValueError("Invalid CSV file data - Not enough rows.")
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 importEntityCSVDialog.importToCanvasCheckbox.isChecked():
sceneToAddTo = self.parent().centralWidget().tabbedPane.getSceneByName(
importEntityCSVDialog.importToCanvasDropdown.currentText())
entityTypeToImportAs = importEntityCSVDialog.entityTypeChoiceDropdown.currentText()
for row in csvDF.itertuples(index=False):
newEntityJSON = {str(attributeRows[key]): str(row[key])
for key in range(len(attributeRows))}
newEntityJSON['Entity Type'] = entityTypeToImportAs
return_results.append(newEntityJSON)
elif importDialog.CSVFileChoiceLinks.isChecked():
csvDF = pd.read_csv(fileDirectory)
# Remove duplicate column names
csvDF = csvDF.loc[:, ~csvDF.columns.duplicated()]
# Fill NaN values with an empty string
csvDF.fillna('')
# If we have less than 2 rows, we cannot import
rowNumber = len(csvDF.index)
if rowNumber < 2:
raise ValueError("Invalid CSV file data - Not enough rows.")
importLinksCSVDialog = ImportLinksFromCSVFile(self, csvDF)
if importLinksCSVDialog.exec_():
entityOneType = importLinksCSVDialog.entityOneTypeChoiceDropdown.currentText()
entityTwoType = importLinksCSVDialog.entityTwoTypeChoiceDropdown.currentText()
for row in csvDF.itertuples(index=False):
count = 0
linkJSON = {}
entityOneJSON = {}
entityTwoJSON = {}
resolutionID = ""
notes = ""
for column in row:
column = str(column)
mapping = importLinksCSVDialog.fieldMappingComboBoxes[count].currentText()
if mapping == 'Entity One':
entityOneJSON = self.parent().LENTDB.getEntityOfType(column, entityOneType)
elif mapping == 'Entity Two':
entityTwoJSON = self.parent().LENTDB.getEntityOfType(column, entityTwoType)
elif mapping == 'Notes':
notes = column
elif mapping == 'Resolution ID':
resolutionID = column
else:
linkJSON[csvDF.columns[count]] = column
count += 1
if (entityOneJSON is not None) and (entityTwoJSON is not None):
linkJSON['uid'] = (entityOneJSON['uid'], entityTwoJSON['uid'])
linkJSON['Notes'] = notes
if importLinksCSVDialog.randAsIs.isChecked():
linkJSON['Resolution'] = resolutionID
elif importLinksCSVDialog.randMerge.isChecked():
linkJSON['Resolution'] = resolutionID + ' | ' + str(uuid4())
elif importLinksCSVDialog.randReplace.isChecked():
linkJSON['Resolution'] = str(uuid4())
self.parent().LENTDB.addLink(linkJSON)
newLinks.append((entityOneJSON['uid'], entityTwoJSON['uid'],
linkJSON['Resolution']))
newNodeUIDs = [newEntity['uid'] for newEntity in self.parent().LENTDB.addEntities(return_results)]
if sceneToAddTo is not None:
for newNodeUID in newNodeUIDs:
if newNodeUID is not None:
sceneToAddTo.addNodeProgrammatic(newNodeUID)
sceneToAddTo.rearrangeGraph()
if newLinks:
self.parent().centralWidget().tabbedPane.addLinksToTabs(newLinks, 'File Links')
self.parent().LENTDB.resetTimeline()
except PermissionError:
self.parent().MESSAGEHANDLER.error('No permission to access the file at the path provided.',
popUp=True, exc_info=False)
except UnicodeDecodeError:
self.parent().MESSAGEHANDLER.error('File path provided points to a binary file that cannot be '
'interpreted as text.', popUp=True, exc_info=False)
except FileNotFoundError:
self.parent().MESSAGEHANDLER.error('File path provided does not point to an existing file.',
popUp=True, exc_info=False)
except TypeError:
self.parent().MESSAGEHANDLER.error('Type Error occurred while processing file. Please ensure that '
'you have selected a file of the appropriate type.',
popUp=True, exc_info=False)
except Exception as e:
self.parent().MESSAGEHANDLER.error('Exception occurred while processing file: ' + str(e),
popUp=True, exc_info=False)
else:
self.parent().MESSAGEHANDLER.error('Invalid file path provided!', popUp=True, exc_info=False)
def savePic(self) -> None:
canvasSaveDialog = CanvasPictureDialog(self)
@@ -1176,40 +1269,184 @@ class ViewAndStopResolutionsDialogOption(QtWidgets.QPushButton):
self.parent().layout().removeRow(self)
class ImportFromFileDialog(QtWidgets.QDialog):
def popupFileDialog(self):
self.fileDirectory = QtWidgets.QFileDialog().getOpenFileName(parent=self, caption='Select file to import',
options=QtWidgets.QFileDialog.DontUseNativeDialog,
filter="CSV or txt (*.csv *.txt)")[0]
if self.fileDirectory != '':
self.fileDirectoryLine.setText(self.fileDirectory)
class ImportLinksFromCSVFile(QtWidgets.QDialog):
def __init__(self, parent):
super(ImportFromFileDialog, self).__init__(parent=parent)
self.fileDirectory = ''
self.setWindowTitle('Import From File')
def __init__(self, parent, csvTableContents: pd.DataFrame):
super(ImportLinksFromCSVFile, self).__init__(parent=parent)
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
self.setModal(True)
importLayout = QtWidgets.QVBoxLayout()
self.setLayout(importLayout)
dialogLayout = QtWidgets.QGridLayout()
self.setLayout(dialogLayout)
descriptionLabel = QtWidgets.QLabel('Choose the filetype you selected')
descriptionLabel.setWordWrap(True)
dialogLayout.addWidget(descriptionLabel, 0, 0, 1, 2)
columnNumber = len(csvTableContents.columns)
self.fileDirectoryButton = QtWidgets.QPushButton("Select file...")
self.fileDirectoryLine = QtWidgets.QLineEdit()
self.fileDirectoryLine.setReadOnly(True)
self.textFileChoice = QtWidgets.QRadioButton('Text file')
self.textFileChoice.setStyleSheet(Stylesheets.RADIO_BUTTON_STYLESHEET)
self.textFileChoice.setChecked(True)
self.CSVFileChoice = QtWidgets.QRadioButton('CSV (Please see documentation for formatting)')
self.CSVFileChoice.setStyleSheet(Stylesheets.RADIO_BUTTON_STYLESHEET)
titleLabel = QtWidgets.QLabel("Import Links from CSV")
titleLabel.setAlignment(QtCore.Qt.AlignCenter)
dialogLayout.addWidget(self.fileDirectoryLine, 1, 0, 1, 2)
dialogLayout.addWidget(self.fileDirectoryButton, 2, 0, 1, 2)
dialogLayout.addWidget(self.textFileChoice, 3, 0, 1, 2)
dialogLayout.addWidget(self.CSVFileChoice, 4, 0, 1, 2)
descLabel = QtWidgets.QLabel("Select the Entity Type of the Parent entity (Entity One) and the Entity Type of "
"the Child entity (Entity Two). Then, map these entities to columns in the CSV "
"file. You may also map the rest of the fields to the other columns of the CSV "
"file. Lastly, choose how to import and / or generate the ID for each Resolution.")
descLabel.setWordWrap(True)
entityOneTypeChoiceWidget = QtWidgets.QWidget()
entityOneTypeChoiceLayout = QtWidgets.QHBoxLayout()
entityOneTypeChoiceWidget.setLayout(entityOneTypeChoiceLayout)
entityOneTypeChoiceLabel = QtWidgets.QLabel("Entity One type:")
self.entityOneTypeChoiceDropdown = QtWidgets.QComboBox()
self.entityOneTypeChoiceDropdown.setEditable(False)
self.entityOneTypeChoiceDropdown.addItems(parent.parent().RESOURCEHANDLER.getAllEntities())
entityOneTypeChoiceLayout.addWidget(entityOneTypeChoiceLabel)
entityOneTypeChoiceLayout.addWidget(self.entityOneTypeChoiceDropdown)
entityTwoTypeChoiceWidget = QtWidgets.QWidget()
entityTwoTypeChoiceLayout = QtWidgets.QHBoxLayout()
entityTwoTypeChoiceWidget.setLayout(entityTwoTypeChoiceLayout)
entityTwoTypeChoiceLabel = QtWidgets.QLabel("Entity Two type:")
self.entityTwoTypeChoiceDropdown = QtWidgets.QComboBox()
self.entityTwoTypeChoiceDropdown.setEditable(False)
self.entityTwoTypeChoiceDropdown.addItems(parent.parent().RESOURCEHANDLER.getAllEntities())
entityTwoTypeChoiceLayout.addWidget(entityTwoTypeChoiceLabel)
entityTwoTypeChoiceLayout.addWidget(self.entityTwoTypeChoiceDropdown)
self.fieldMappingComboBoxes = []
tableFieldAttributeMapping = QtWidgets.QWidget()
tableFieldAttributeMappingLayout = QtWidgets.QHBoxLayout()
tableFieldAttributeMapping.setLayout(tableFieldAttributeMappingLayout)
for fieldIndex in range(columnNumber):
fieldMappingWidget = QtWidgets.QComboBox()
fieldMappingWidget.setEditable(False)
fieldMappingWidget.currentIndexChanged.connect(self.changeMappingForField)
fieldMappingWidget.addItems(['', 'Entity One', 'Entity Two', 'Resolution ID', 'Notes'])
self.fieldMappingComboBoxes.append(fieldMappingWidget)
tableFieldAttributeMappingLayout.addWidget(fieldMappingWidget)
csvTable = QtWidgets.QTableWidget(3, columnNumber, self)
csvTable.setHorizontalHeaderLabels(csvTableContents.columns)
for row in csvTableContents[:3].itertuples():
rowValues = list(row)
for column in range(columnNumber):
columnItem = QtWidgets.QTableWidgetItem(str(rowValues[column + 1]))
columnItem.setFlags(columnItem.flags() & ~QtCore.Qt.ItemIsEditable)
csvTable.setItem(rowValues[0], column, columnItem)
randomizationLabel = QtWidgets.QLabel("If the resolution identifiers (i.e. 'Resolution ID') are not guaranteed "
"to be unique, you can configure whether you'd like to leave them as-is, "
"append a random token, or ignore any resolution identifiers and just "
"have random tokens as the Resolution IDs.\nPlease select what you would "
"like to do:")
randomizationLabel.setWordWrap(True)
randomizationLabel.setAlignment(QtCore.Qt.AlignCenter)
self.randAsIs = QtWidgets.QRadioButton("Use 'Resolution ID' as-is.")
self.randMerge = QtWidgets.QRadioButton("Append a random token to the values mapped to the 'Resolution ID' "
"field.")
self.randReplace = QtWidgets.QRadioButton("Ignore the 'Resolution ID' mapping, instead generate random values "
"for Resolution IDs.")
self.randAsIs.setChecked(True)
buttonsWidget = QtWidgets.QWidget()
buttonsWidgetLayout = QtWidgets.QHBoxLayout()
buttonsWidget.setLayout(buttonsWidgetLayout)
confirmButton = QtWidgets.QPushButton('Confirm')
cancelButton = QtWidgets.QPushButton('Cancel')
buttonsWidgetLayout.addWidget(cancelButton)
buttonsWidgetLayout.addWidget(confirmButton)
cancelButton.clicked.connect(self.reject)
confirmButton.clicked.connect(self.checkIfEntitiesAreMapped)
importLayout.addWidget(titleLabel)
importLayout.addWidget(descLabel)
importLayout.addWidget(entityOneTypeChoiceWidget)
importLayout.addWidget(entityTwoTypeChoiceWidget)
importLayout.addWidget(tableFieldAttributeMapping)
importLayout.addWidget(csvTable)
importLayout.addWidget(randomizationLabel)
importLayout.addWidget(self.randAsIs)
importLayout.addWidget(self.randMerge)
importLayout.addWidget(self.randReplace)
importLayout.addWidget(buttonsWidget)
def changeMappingForField(self, newIndex):
for comboBox in self.fieldMappingComboBoxes:
if comboBox.currentIndex() == newIndex and not comboBox.hasFocus():
comboBox.setCurrentIndex(0)
def checkIfEntitiesAreMapped(self):
# Check if Entity One and Entity Two labels are assigned, do not proceed if not.
isOneAssigned = False
isTwoAssigned = False
for comboBox in self.fieldMappingComboBoxes:
if comboBox.currentIndex() == 1:
isOneAssigned = True
elif comboBox.currentIndex() == 2:
isTwoAssigned = True
if isOneAssigned and isTwoAssigned:
self.accept()
else:
self.parent().parent().MESSAGEHANDLER.warning('Need to configure mappings for Entity One (parent) and '
'Entity Two (child) before proceeding.', popUp=True)
class ImportEntityFromCSVFile(QtWidgets.QDialog):
def __init__(self, parent, csvTableContents: pd.DataFrame):
super(ImportEntityFromCSVFile, self).__init__(parent=parent)
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
self.setModal(True)
importLayout = QtWidgets.QVBoxLayout()
self.setLayout(importLayout)
columnNumber = len(csvTableContents.columns)
titleLabel = QtWidgets.QLabel("Import Entities from CSV")
titleLabel.setAlignment(QtCore.Qt.AlignCenter)
descLabel = QtWidgets.QLabel("Select the entity type to import the entities from the CSV file as. "
"Then, configure the mapping between the CSV fields and entity attributes. To do "
"so, select from the drop-down boxes the entity attributes that correspond to "
"each field in the CSV file. The drop-down boxes left blank will have the "
"corresponding CSV column names be treated as attribute names for the new "
"entities.")
descLabel.setWordWrap(True)
entityTypeChoiceWidget = QtWidgets.QWidget()
entityTypeChoiceLayout = QtWidgets.QHBoxLayout()
entityTypeChoiceWidget.setLayout(entityTypeChoiceLayout)
entityTypeChoiceLabel = QtWidgets.QLabel("Entity Type to Import As:")
self.entityTypeChoiceDropdown = QtWidgets.QComboBox()
self.entityTypeChoiceDropdown.setEditable(False)
self.entityTypeChoiceDropdown.addItems(parent.parent().RESOURCEHANDLER.getAllEntities())
self.entityTypeChoiceDropdown.currentIndexChanged.connect(self.pickEntityToImportAs)
entityTypeChoiceLayout.addWidget(entityTypeChoiceLabel)
entityTypeChoiceLayout.addWidget(self.entityTypeChoiceDropdown)
self.fieldMappingComboBoxes = []
tableFieldAttributeMapping = QtWidgets.QWidget()
tableFieldAttributeMappingLayout = QtWidgets.QHBoxLayout()
tableFieldAttributeMapping.setLayout(tableFieldAttributeMappingLayout)
for fieldIndex in range(columnNumber):
fieldMappingWidget = QtWidgets.QComboBox()
fieldMappingWidget.setEditable(False)
fieldMappingWidget.currentIndexChanged.connect(self.changeMappingForField)
fieldMappingWidget.addItem('')
self.fieldMappingComboBoxes.append(fieldMappingWidget)
tableFieldAttributeMappingLayout.addWidget(fieldMappingWidget)
csvTable = QtWidgets.QTableWidget(3, columnNumber, self)
csvTable.setHorizontalHeaderLabels(csvTableContents.columns)
for row in csvTableContents[:3].itertuples():
rowValues = list(row)
for column in range(columnNumber):
columnItem = QtWidgets.QTableWidgetItem(str(rowValues[column + 1]))
columnItem.setFlags(columnItem.flags() & ~QtCore.Qt.ItemIsEditable)
csvTable.setItem(rowValues[0], column, columnItem)
importToCanvasChoiceWidget = QtWidgets.QWidget()
importToCanvasChoiceLayout = QtWidgets.QHBoxLayout()
importToCanvasChoiceWidget.setLayout(importToCanvasChoiceLayout)
self.importToCanvasCheckbox = QtWidgets.QCheckBox('Import To Canvas:')
self.importToCanvasCheckbox.setStyleSheet(Stylesheets.CHECK_BOX_STYLESHEET)
self.importToCanvasDropdown = QtWidgets.QComboBox()
@@ -1218,8 +1455,95 @@ class ImportFromFileDialog(QtWidgets.QDialog):
self.importToCanvasDropdown.setDisabled(True)
self.importToCanvasCheckbox.toggled.connect(lambda: self.importToCanvasDropdown.setDisabled(
self.importToCanvasDropdown.isEnabled()))
dialogLayout.addWidget(self.importToCanvasCheckbox, 5, 0, 1, 1)
dialogLayout.addWidget(self.importToCanvasDropdown, 5, 1, 1, 1)
importToCanvasChoiceLayout.addWidget(self.importToCanvasCheckbox)
importToCanvasChoiceLayout.addWidget(self.importToCanvasDropdown)
buttonsWidget = QtWidgets.QWidget()
buttonsWidgetLayout = QtWidgets.QHBoxLayout()
buttonsWidget.setLayout(buttonsWidgetLayout)
confirmButton = QtWidgets.QPushButton('Confirm')
cancelButton = QtWidgets.QPushButton('Cancel')
buttonsWidgetLayout.addWidget(cancelButton)
buttonsWidgetLayout.addWidget(confirmButton)
cancelButton.clicked.connect(self.reject)
confirmButton.clicked.connect(self.confirmThatPrimaryFieldIsMapped)
importLayout.addWidget(titleLabel)
importLayout.addWidget(descLabel)
importLayout.addWidget(entityTypeChoiceWidget)
importLayout.addWidget(tableFieldAttributeMapping)
importLayout.addWidget(csvTable)
importLayout.addWidget(importToCanvasChoiceWidget)
importLayout.addWidget(buttonsWidget)
self.pickEntityToImportAs()
def pickEntityToImportAs(self):
currentEntityAttributes = [''] + self.parent().parent().RESOURCEHANDLER.getEntityAttributes(
self.entityTypeChoiceDropdown.currentText())
for comboBox in self.fieldMappingComboBoxes:
comboBox.clear()
comboBox.addItems(currentEntityAttributes)
def changeMappingForField(self, newIndex):
for comboBox in self.fieldMappingComboBoxes:
if comboBox.currentIndex() == newIndex and not comboBox.hasFocus():
comboBox.setCurrentIndex(0)
def confirmThatPrimaryFieldIsMapped(self):
primaryField = self.parent().parent().RESOURCEHANDLER.getPrimaryFieldForEntityType(
self.entityTypeChoiceDropdown.currentText())
for comboBox in self.fieldMappingComboBoxes:
if comboBox.currentText() == primaryField:
self.accept()
self.parent().parent().MESSAGEHANDLER.warning('Primary field (' + primaryField +
') needs to be mapped before proceeding.')
class ImportFromFileDialog(QtWidgets.QDialog):
def popupFileDialog(self):
self.fileDirectory = QtWidgets.QFileDialog().getOpenFileName(parent=self, caption='Select file to import',
dir=str(Path.home()),
options=QtWidgets.QFileDialog.DontUseNativeDialog,
filter="CSV or txt (*.csv *.txt)")[0]
if self.fileDirectory != '':
self.fileDirectoryLine.setText(self.fileDirectory)
def __init__(self, parent):
super(ImportFromFileDialog, self).__init__(parent=parent)
self.fileDirectory = ''
self.setWindowTitle('Import Entities From Text File')
self.setModal(True)
dialogLayout = QtWidgets.QGridLayout()
self.setLayout(dialogLayout)
descriptionLabel = QtWidgets.QLabel('Select the file to import Entities or Links from:')
descriptionLabel.setAlignment(QtCore.Qt.AlignCenter)
descriptionLabel.setWordWrap(True)
dialogLayout.addWidget(descriptionLabel, 0, 0, 1, 2)
self.fileDirectoryButton = QtWidgets.QPushButton("Select file...")
self.fileDirectoryLine = QtWidgets.QLineEdit()
self.fileDirectoryLine.setReadOnly(True)
fileChoiceLabel = QtWidgets.QLabel('Specify the type of the chosen file and what to import:')
fileChoiceLabel.setAlignment(QtCore.Qt.AlignCenter)
fileChoiceLabel.setWordWrap(True)
self.textFileChoice = QtWidgets.QRadioButton('Text file - Entities Import')
self.textFileChoice.setStyleSheet(Stylesheets.RADIO_BUTTON_STYLESHEET)
self.textFileChoice.setChecked(True)
self.CSVFileChoice = QtWidgets.QRadioButton('CSV - Entities Import')
self.CSVFileChoice.setStyleSheet(Stylesheets.RADIO_BUTTON_STYLESHEET)
self.CSVFileChoiceLinks = QtWidgets.QRadioButton('CSV - Links Import')
self.CSVFileChoiceLinks.setStyleSheet(Stylesheets.RADIO_BUTTON_STYLESHEET)
dialogLayout.addWidget(self.fileDirectoryLine, 1, 0, 1, 2)
dialogLayout.addWidget(self.fileDirectoryButton, 2, 0, 1, 2)
dialogLayout.addWidget(fileChoiceLabel, 3, 0, 1, 2)
dialogLayout.addWidget(self.textFileChoice, 4, 0, 1, 2)
dialogLayout.addWidget(self.CSVFileChoice, 5, 0, 1, 2)
dialogLayout.addWidget(self.CSVFileChoiceLinks, 6, 0, 1, 2)
acceptButton = QtWidgets.QPushButton('Accept')
acceptButton.setAutoDefault(True)
@@ -1235,28 +1559,64 @@ class ImportFromFileDialog(QtWidgets.QDialog):
self.setMaximumHeight(300)
self.setMinimumHeight(300)
dialogLayout.addWidget(cancelButton, 6, 0, 1, 1)
dialogLayout.addWidget(acceptButton, 6, 1, 1, 1)
dialogLayout.addWidget(cancelButton, 7, 0, 1, 1)
dialogLayout.addWidget(acceptButton, 7, 1, 1, 1)
class ImportFromTextFileDialog(QtWidgets.QDialog):
def __init__(self, parent):
def __init__(self, parent, fileContents):
super(ImportFromTextFileDialog, self).__init__(parent=parent)
self.setWindowTitle('Import From Text File')
self.setModal(True)
dialogLayout = QtWidgets.QGridLayout()
self.setLayout(dialogLayout)
descriptionLabel = QtWidgets.QLabel('Choose what type the entities will be imported as')
descriptionLabel = QtWidgets.QLabel('Importing entities from text file, one entity per line.')
descriptionLabel.setAlignment(QtCore.Qt.AlignCenter)
descriptionLabel.setWordWrap(True)
dialogLayout.addWidget(descriptionLabel, 0, 0, 1, 2)
importTypeLabel = QtWidgets.QLabel('Choose the Entity Type to import as:')
importTypeLabel.setWordWrap(True)
dialogLayout.addWidget(importTypeLabel, 1, 0, 1, 1)
self.importTypeDropdown = QtWidgets.QComboBox()
self.importTypeDropdown.addItems(parent.parent().RESOURCEHANDLER.getAllEntities())
self.importTypeDropdown.setEditable(False)
dialogLayout.addWidget(self.importTypeDropdown, 1, 1, 1, 1)
self.importTypeDropdown.currentTextChanged.connect(self.updatePrimaryFieldValueLabel)
dialogLayout.addWidget(self.importTypeDropdown, 3, 1, 1, 1)
typePrimaryFieldLabel = QtWidgets.QLabel('Primary Field for chosen type:')
typePrimaryFieldLabel.setWordWrap(False)
dialogLayout.addWidget(typePrimaryFieldLabel, 2, 0, 1, 1)
self.typePrimaryFieldValueLabel = QtWidgets.QLineEdit('')
self.typePrimaryFieldValueLabel.setReadOnly(True)
dialogLayout.addWidget(self.typePrimaryFieldValueLabel, 2, 1, 1, 1)
textTable = QtWidgets.QTableWidget(3, 1, self)
textTable.setWordWrap(True)
textTable.setFixedWidth(450)
for line in range(len(fileContents)):
columnItem = QtWidgets.QTableWidgetItem(fileContents[line])
columnItem.setFlags(columnItem.flags() & ~QtCore.Qt.ItemIsEditable)
textTable.setItem(line, 0, columnItem)
textTable.setColumnWidth(0, 450)
textTable.setHorizontalHeaderLabels(['File Entities Preview'])
dialogLayout.addWidget(textTable, 3, 0, 1, 2)
self.importToCanvasCheckbox = QtWidgets.QCheckBox('Import To Canvas:')
self.importToCanvasCheckbox.setStyleSheet(Stylesheets.CHECK_BOX_STYLESHEET)
self.importToCanvasDropdown = QtWidgets.QComboBox()
self.importToCanvasDropdown.addItems(list(parent.parent().centralWidget().tabbedPane.canvasTabs))
self.importToCanvasDropdown.setEditable(False)
self.importToCanvasDropdown.setDisabled(True)
self.importToCanvasCheckbox.toggled.connect(lambda: self.importToCanvasDropdown.setDisabled(
self.importToCanvasDropdown.isEnabled()))
dialogLayout.addWidget(self.importToCanvasCheckbox, 4, 0, 1, 1)
dialogLayout.addWidget(self.importToCanvasDropdown, 4, 1, 1, 1)
acceptButton = QtWidgets.QPushButton('Accept')
acceptButton.setAutoDefault(True)
@@ -1266,13 +1626,19 @@ class ImportFromTextFileDialog(QtWidgets.QDialog):
cancelButton.clicked.connect(self.reject)
acceptButton.setFocus()
self.setMaximumWidth(450)
self.setMinimumWidth(300)
self.setMaximumHeight(300)
self.setMinimumHeight(300)
self.setFixedWidth(472)
self.setFixedHeight(300)
dialogLayout.addWidget(cancelButton, 4, 0, 1, 1)
dialogLayout.addWidget(acceptButton, 4, 1, 1, 1)
dialogLayout.addWidget(cancelButton, 5, 0, 1, 1)
dialogLayout.addWidget(acceptButton, 5, 1, 1, 1)
# Initialize the primary field value label to whatever is the primary field of the first entity
# in the drop down selection box.
self.updatePrimaryFieldValueLabel()
def updatePrimaryFieldValueLabel(self):
self.typePrimaryFieldValueLabel.setText(str(self.parent().parent().RESOURCEHANDLER.getPrimaryFieldForEntityType(
self.importTypeDropdown.currentText())))
class CanvasPictureDialog(QtWidgets.QDialog):

View File

@@ -168,7 +168,8 @@ class ResourceHandler:
eJson['Icon'] = self.getEntityDefaultPicture(entityType)
if jsonData is not None:
for key in eJson:
# Allow setting of attributes that are not defined in the Entity specification.
for key in jsonData:
value = jsonData.get(key)
if value is not None and value != '':
eJson[key] = value
@@ -181,7 +182,7 @@ class ResourceHandler:
return eJson
def getPrimaryFieldForEntityType(self, entityType: str):
def getPrimaryFieldForEntityType(self, entityType: str) -> Union[str, None]:
try:
for category in self.entityCategoryList:
if entityType in self.entityCategoryList[category]:
@@ -207,7 +208,7 @@ class ResourceHandler:
return eJson
def getLinkJson(self, jsonData) -> Union[dict, None]:
def getLinkJson(self, jsonData: dict) -> Union[dict, None]:
linkJson = {}
try:
linkJson['uid'] = jsonData['uid']
@@ -222,6 +223,11 @@ class ResourceHandler:
linkJson['Date Last Edited'] = utcNow
linkJson['Notes'] = str(jsonData.get('Notes'))
# Transfer all values from jsonData to linkJson, but preserve the values and order of linkJson for existing
# keys.
jsonData.update(linkJson)
linkJson.update(jsonData)
return linkJson
def getEntityDefaultPicture(self, entityType) -> QByteArray:

View File

@@ -339,10 +339,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.linkingNodes = True
def deleteSpecificEntity(self, itemUID: str) -> None:
for canvas in self.centralWidget().tabbedPane.canvasTabs:
scene = self.centralWidget().tabbedPane.canvasTabs[canvas].scene()
if itemUID in scene.nodesDict:
scene.removeNode(scene.nodesDict[itemUID])
self.centralWidget().tabbedPane.nodeRemoveAllHelper(itemUID)
self.LENTDB.removeEntity(itemUID)
self.MESSAGEHANDLER.info("Deleted node: " + itemUID)
@@ -452,7 +449,9 @@ class MainWindow(QtWidgets.QMainWindow):
shortestPath = None
if shortestPath is None:
self.setStatus('No path between the selected nodes was found.')
messagePathNotFound = 'No path found connecting the selected nodes: ' + str(endPoints)
self.setStatus(messagePathNotFound)
self.MESSAGEHANDLER.info(messagePathNotFound, popUp=True)
else:
self.centralWidget().tabbedPane.getCurrentScene().clearSelection()
for item in [node for node in self.centralWidget().tabbedPane.getCurrentScene().items()
@@ -462,7 +461,7 @@ class MainWindow(QtWidgets.QMainWindow):
linksToSelect = [(a, b) for a, b in zip(shortestPath, shortestPath[1:])]
for linkItem in [link for link in self.centralWidget().tabbedPane.getCurrentScene().items()
if isinstance(link, BaseConnector)]:
if linkItem.uid in linksToSelect:
if linkItem.uid.intersection(linksToSelect):
linkItem.setSelected(True)
self.setStatus('Shortest path found.')
@@ -470,34 +469,38 @@ class MainWindow(QtWidgets.QMainWindow):
currentScene = self.centralWidget().tabbedPane.getCurrentScene()
currentUIDs = [item.uid for item in currentScene.items() if isinstance(item, BaseNode)
or isinstance(item, BaseConnector)]
entityPrimaryFields = []
entityPrimaryFields = {}
for uid in currentUIDs:
item = self.LENTDB.getEntity(uid)
if item is None:
item = self.LENTDB.getLink(uid)
if item is None:
continue
entityPrimaryFields.append(item[list(item)[1]])
matchedPrimaryFieldsAndUIDs = [item for item in zip(currentUIDs, entityPrimaryFields)]
# Remove duplicates
entityPrimaryFields = list(set(entityPrimaryFields))
matchedPrimaryFieldsAndUIDs = list(set(matchedPrimaryFieldsAndUIDs))
findPrompt = FindEntityOnCanvasDialog(entityPrimaryFields)
promptReturnCode = findPrompt.exec()
if isinstance(uid, str):
item = self.LENTDB.getEntity(uid)
if item is not None:
if not entityPrimaryFields.get(item[list(item)[1]]):
entityPrimaryFields[item[list(item)[1]]] = set()
entityPrimaryFields[item[list(item)[1]]].add(uid)
if promptReturnCode:
elif isinstance(uid, set):
for potentialLinkItem in uid:
item = self.LENTDB.getLink(potentialLinkItem)
if item is not None:
if not entityPrimaryFields.get(item['Resolution']):
entityPrimaryFields[item['Resolution']] = set()
entityPrimaryFields[item['Resolution']].add(str(uid))
findPrompt = FindEntityOnCanvasDialog(list(entityPrimaryFields))
if findPrompt.exec():
uidsToSelect = []
findText = findPrompt.findInput.text()
for item in matchedPrimaryFieldsAndUIDs:
if item[1].startswith(findText):
uidsToSelect.append(item[0])
for item in entityPrimaryFields:
if item.startswith(findText):
# Add the elements in each index to uidsToSelect instead of the sets themselves.
uidsToSelect.extend(entityPrimaryFields[item])
currentScene.clearSelection()
for item in [linkOrEntity for linkOrEntity in currentScene.items()
if isinstance(linkOrEntity, BaseNode) or isinstance(linkOrEntity, BaseConnector)]:
if item.uid in uidsToSelect:
if str(item.uid) in uidsToSelect:
item.setSelected(True)
if len(uidsToSelect) == 1:
if len(uidsToSelect) == 1 and ',' not in uidsToSelect[0]:
self.centralWidget().tabbedPane.getCurrentView().centerViewportOnNode(uidsToSelect[0])
def mergeEntities(self) -> None: