class RelationInstance(): ''' This represents one NodeEra managed relationship that can be on one or more diagrams. This class manages reading and writing the relationship to Neo4j The RelationItem class creates the arc graphics item and the text graphics item and draws them on the scene. startNode and endNode must be a node instance ''' def __init__(self, parent, relationInstanceDict=None, startNode=None, endNode=None, model=None): self.helper = Helper() self.neoTypeFunc = NeoTypeFunc() self.diagramType = "Instance Relationship" self.logMsg = None self.relationInstanceDict = relationInstanceDict self.parent = parent self.model = model self.neoCon = self.model.modelNeoCon self.relationship = None # set local variables to what is in the dictionary if self.relationInstanceDict: self.relTemplate = self.relationInstanceDict.get("relTemplate", None) self.relName = self.relationInstanceDict.get("relName", "NoRelationshipName") self.propList = self.relationInstanceDict.get("properties", []) self.startNZID = self.relationInstanceDict.get("startNZID", None) self.endNZID = self.relationInstanceDict.get("endNZID", None) self.startNode = None self.endNode = None if self.relationInstanceDict.get("NZID", None) == None: self.NZID = str(uuid.uuid4()) else: self.NZID = self.relationInstanceDict["NZID"] self.neoID=self.relationInstanceDict.get("neoID", None) # if no dictionary passed then set defaults else: self.relName = "NoRelationshipName" self.relTemplate = None self.propList = [] self.NZID = str(uuid.uuid4()) self.neoID=None self.startNZID = None self.endNZID = None self.startNode = None self.endNode = None # get start and end node item objects if not startNode is None: self.startNode = startNode self.startNZID = self.startNode.NZID if not endNode is None: self.endNode = endNode self.endNZID = self.endNode.NZID self.getFormat() if not self.relationInstanceDict: self.relationInstanceDict = self.getObjectDict() def reloadDictValues(self, ): ''' This function will reset the local variables to the instance rel dictionary. This is needed when open diagram relationship instances need to be updated. ''' index, relDict = self.model.getDictByName(topLevel="Instance Relationship",objectName=self.NZID) if relDict: # set local variables to what is in the dictionary self.relInstanceDict = relDict self.nodeTemplate = self.relInstanceDict.get("relTemplate", None) self.propList = self.relInstanceDict.get("properties", []) self.NZID = self.relInstanceDict["NZID"] self.neoID=self.relInstanceDict.get("neoID", None) def getFormat(self, ): ''' determine if the rel instance has a template format or should use the project default format ''' # get the default self.relFormat = IRelFormat(formatDict=self.model.modelData["IRformat"]) # get a custom template format if there is one if not self.relTemplate is None: index, relTemplateDict = self.model.getDictByName(topLevel="Relationship Template",objectName=self.relTemplate) if not relTemplateDict is None: self.instanceRelFormatDict = relTemplateDict.get("IRformat", None) if not self.instanceRelFormatDict is None: self.relFormat = IRelFormat(formatDict=self.instanceRelFormatDict) def getObjectDict(self, ): objectDict = {} objectDict["NZID"] = self.NZID objectDict["name"] = self.NZID objectDict["neoID"] = self.neoID objectDict["diagramType"] = self.diagramType objectDict["relName"] = self.relName objectDict["properties"] = self.propList objectDict["startNZID"] = self.startNZID objectDict["endNZID"] = self.endNZID objectDict["relTemplate"] = self.relTemplate self.genRelSignature(objectDict) return objectDict def genRelSignature(self, relDict): '''generate the display name for a relationship instance ''' if self.neoID is None: relID = self.NZID else: relID = self.neoID if (self.startNode is None or self.startNode.neoID is None): fromID = "0" else: fromID = self.startNode.neoID if (self.endNode is None or self.startNode.neoID is None): toID = "0" else: toID = self.endNode.neoID propList = self.helper.genPropValueList("n", relDict) signature = "({})-[{}:{} {}{}{}]->({})".format(str(fromID), str(relID), self.relName, "{", propList, "}", str(toID)) relDict["displayName"] = signature def setLogMethod(self, logMethod=None): if logMethod is None: if self.logMsg is None: self.logMsg = self.noLog else: self.logMsg = logMethod def noLog(self, msg ): return def deleteRel(self, logMethod=None): '''This method deletes the relationship from the database.''' # set the logging method self.setLogMethod(logMethod) # if it doesn't have a neoID it's never been created in the db so nothing to delete if self.neoID is None: self.logMsg("The Relationship does not exist in the database. Cannot delete.") return True, "No Relationship to delete." # delete the Relationship based on the neoID cypher = "match ()-[r]->() where id(r) = " + str(self.neoID) + " delete r" rc, msg = self.neoCon.runCypherAuto(cypher) self.logMsg(msg) return rc, msg def checkRelationship(self, logMethod=None): '''This method checks to see if a matching relationship exists in the database. ''' # set the logging method self.setLogMethod(logMethod) # if it doesn't have a neoID it's never been created in the db so create a blank one if self.neoID is None: rc = NOTFOUND msg = "Diagram Relationship Doesn't Exist in the Graph" return rc, msg # see if the node still exists in the db based on the node id cypher = "match ()-[r]->() where id(r) = " + str(self.neoID) + " return r" rc, msg = self.neoCon.runCypherExplicit(cypher) if rc is False: self.logMsg(msg) msg = "Error Querying Graph." return NOTFOUND, msg else: if len(self.neoCon.resultSet) > 0: firstRec = self.neoCon.resultSet[0] if not firstRec is None: self.relationship = firstRec["r"] # the rel was found so set it if self.compareRel(): return FOUNDMATCH, "Diagram Relationship Matches Graph" else: return FOUNDNOMATCH, "Diagram Relationship Doesn't Match Graph" else: return NOTFOUND, "Diagram Relationship Doesn't Exist in Graph" else: return NOTFOUND, "Diagram Relationship Doesn't Exist in Graph" return def compareRel(self): '''Compare the Relationship object retrieved from the database with the current state of the relationship item''' rc = True # need to check if the start and end nodes are the same pass # compare graph properties to diagram properties for propName, propVal in dict(self.relationship).items(): try: dataType = self.neoTypeFunc.getNeo4jDataType(value=propVal) dataVal = self.neoTypeFunc.convertTypeToString(propVal) # special case as an integer is also a valid float if dataType == "Integer": if ([propName, "Integer", dataVal] in self.propList or [propName, "Float", dataVal] in self.propList): continue else: rc = False break elif [propName, dataType, dataVal] in self.propList: continue else: rc = False break except: rc = False break # compare diagram properties to graph properties for prop in self.propList: try: # is the property name one of the keys in the node if prop[0] in [propName for propName, propVal in dict(self.relationship).items() ]: pVal = [propVal for propName, propVal in dict(self.relationship).items() if propName == prop[0]][0] # does the datatype and data value match - special case for Float because the value could be an int or a float if (prop[1] == "Float" and self.neoTypeFunc.getNeo4jDataType(value=pVal) in ["Float", "Integer"] and prop[2] == self.neoTypeFunc.convertTypeToString(pVal) ): continue elif (prop[1] == self.neoTypeFunc.getNeo4jDataType(value=pVal) and prop[2] == self.neoTypeFunc.convertTypeToString(pVal)): continue else: rc = False break else: # is the prop missing in the neo4j relationship because it's null on the diagram? if prop[2] == "Null": continue else: rc = False break except: rc = False break return rc def getRelationship(self, logMethod=None): ''' This method retrieves the relationship from Neo4j and sets the relationship object. At this point the relationship object looks like it is in Neo4j ''' # set the logging method self.setLogMethod(logMethod) # if there's no neoID then there isn't a relationship in neo4j if self.neoID is None: return None, "Relationship [{}] doesn't exist in Neo4j".format(self.neoID) # try to retrieve the relationship based on the neoid cypher = "match ()-[r]->() where id(r) = " + str(self.neoID) + " return r" rc, msg = self.neoCon.runCypherAuto(cypher) if rc is False: return rc, "Retrieve Relationship [{}] error - {}".format(self.neoID, msg) else: # relationship found, save it firstRec = None if len(self.neoCon.resultSet) > 0: firstRec = self.neoCon.resultSet[0] if not firstRec is None: self.relationship = firstRec["r"] # the rel was found so set it return True, "Relationship [{}] Found".format(self.neoID) # relationship not found, it might have been deleted thru the backend else: return None, "Relationship [{}] Not Found".format(self.neoID) def createNewRelationship(self, logMethod=None): ''' create a new relationship in neo4j based on the current state of the relation instance object ''' # set the logging method self.setLogMethod(logMethod) # force creation of the start and end nodes in Neo4j if they don't exist rc, msg = self.startNode.getNode(logMethod) if rc == False: return rc, "Create Relationship error on from node - {}".format(msg) rc, msg = self.endNode.getNode(logMethod) if rc == False: return rc, "Create Relationship error on to node - {}".format(msg) # generate cypher stmt to create the relationship cypher = self.helper.genCreateRelCypher(relInstanceDict = self.getObjectDict(), rel = self.relationship, fromNeoID = self.startNode.neoID, toNeoID=self.endNode.neoID) # run the create rc, msg = self.neoCon.runCypherAuto(cypher) if rc is True: firstRec = None if len(self.neoCon.resultSet) > 0: firstRec = self.neoCon.resultSet[0] if not firstRec is None: self.relationship = firstRec["r"] # the rel was found so set it self.neoID = firstRec["id(r)"] self.relationInstanceDict["neoID"] = self.neoID return rc, "New Relationship Created - {}".format(msg) else: self.logMsg(msg) return rc, "Create New Relationship [{}] error - {}".format(self.NZID, msg) return False, "Unexpected Error" def updateRelProps(self, ): ''' set the properties of the relationship object to what has been entered on the dialog ''' # properties relProps = dict(self.relationship) #set all existing property values in the relationship object to None for key in relProps: self.relationship[key]=None # set all property values from the grid for property in self.propList: self.relationship[property[0]]=property[1] def updateRelationship(self, logMethod=None): ''' relationship already exists, so all you can do is change the properties if any ''' # set the logging method self.setLogMethod(logMethod) # generate cypher stmt to create the relationship cypher = self.helper.genUpdateRelCypher(relInstanceDict = self.getObjectDict(), rel = self.relationship, relID = self.neoID) # run the create rc, msg = self.neoCon.runCypherAuto(cypher) if rc is True: return rc, "Existing Relationship Updated - {}".format(msg) else: self.logMsg(msg) return rc, "Update Existing Relationship [{}] error - {}".format(self.NZID, msg) def syncToDB(self, logMethod=None): # get the relationship from the db, rc, msg = self.getRelationship(logMethod) if rc is True: # relationship found in neo4j and relationship object set. generate cypher to update relationship to look like the ui rc, msg = self.updateRelationship(logMethod) if rc is True: return rc, "Sync Relationship OK - {}".format(msg) else: return rc, "Sync Relationship Error - {}".format(msg) elif rc is False: # error processing the match statement - big fail return rc, "Sync Relationship Error - {}".format(msg) elif rc is None: # relationship object was not found in neo4j so create it rc, msg = self.createNewRelationship(logMethod) if rc is True: return rc, "Sync Relationship OK - {}".format(msg) else: return rc, "Sync Relationship Error - {}".format(msg) else: return rc, "Unknown Sync Relationship Error - {}".format(msg) def syncFromDB(self, logMethod=None): # set the logging method self.setLogMethod(logMethod) # should never happen, but if this instance rel has no neoID it can't be matched to a neo4j node so no way to sync it from neo4j if self.neoID is None: msg = "Error - this instance relationship has never been saved to Neo4j" self.helper.displayErrMsg("Sync From DB", msg) return False, msg rc, msg = self.getRelationship() if rc is True: # get the dictionary of properties from the relationship object and convert it to a list props = [] for key, val in dict(self.relationship).items(): props.append( [key, self.neoTypeFunc.getNeo4jDataType(value=val),self.neoTypeFunc.convertTypeToString(dataValue=val) ]) # set instance props to look like what's in the neo4j relationship object just retrieved self.propList = props # update the object dictionary self.relationInstanceDict = self.getObjectDict() # save it to the design model self.saveToModel() return True, "Instance Relationship Synced" else: return False, "Error syncing Instance Relationship" def saveToModel(self, ): # save the current state of the relation instance to the project model saveIndex = self.model.instanceTopLevelIndex("Instance Relationship", self.NZID) if not saveIndex is None: self.model.modelData["Instance Relationship"][saveIndex]=self.relationInstanceDict
class EditRelDlg(QDialog, Ui_EditRelDlg): """ Class documentation goes here. """ def __init__(self, parent=None): """ Constructor @param parent reference to the parent widget @type QWidget """ super(EditRelDlg, self).__init__(parent) self.setupUi(self) self.parent = parent self.designModel = self.parent.designModel self.templateDict = self.parent.templateDict self.neoCon = self.parent.neoCon self.helper = Helper() self.rel = None self.neoID = None # add edit relationship widet self.editRel = EditRelWidget(self, templateDict=self.templateDict) self.editRelLayout = QVBoxLayout(self.frameBody) self.editRelLayout.setObjectName("editRelLayout") self.editRelLayout.addWidget(self.editRel) # finish UI setup self.txtTitle.setText("Add New Relationship") self.lastAddNeoID = None def logMsg(self, msg): # add message to the log if logging: logging.info(msg) @pyqtSlot() def on_btnAddNew_clicked(self): """ User clicks the add new button so add a new relationship """ # validate the data entry if self.validate(): QApplication.setOverrideCursor(Qt.WaitCursor) try: # create a new relationship selectedFromNode = self.editRel.cmbFromNode.currentText() endIndex = selectedFromNode.find("]") fromNeoID = selectedFromNode[1:endIndex] selectedToNode = self.editRel.cmbToNode.currentText() endIndex = selectedToNode.find("]") toNeoID = selectedToNode[1:endIndex] createCypher = self.helper.genCreateRelCypher( relInstanceDict=self.getObjectDict(), fromNeoID=fromNeoID, toNeoID=toNeoID) rc, msg = self.neoCon.runCypherExplicit(createCypher) QApplication.restoreOverrideCursor() if not rc == True: self.helper.displayErrMsg("Create New Relationship", msg) else: firstRec = self.neoCon.resultSet[0] if not firstRec is None: self.neoID = firstRec["id(r)"] self.rel = firstRec["r"] self.helper.displayErrMsg("Create New Relationship", "New Relationship Created.") except BaseException as e: self.helper.displayErrMsg( "Create New Relationship", "Error creating Relationship - {}".format(repr(e))) finally: QApplication.restoreOverrideCursor() def getObjectDict(self, ): ''' This function returns a dictionary with all the data entered on the UI ''' objectDict = {} objectDict["neoID"] = self.neoID objectDict["relName"] = self.templateDict["relname"] #save the attributes propList = [] model = self.editRel.gridProps.model() numrows = model.rowCount() for row in range(0, numrows): relProp = [ model.item(row, PROPERTY).data(Qt.EditRole), model.item(row, DATATYPE).data(Qt.EditRole), model.item(row, VALUE).data(Qt.EditRole) ] # only include properties that aren't null propList.append(relProp) objectDict["properties"] = propList return objectDict def validate(self, ): # make sure all properties have valid datatypes # xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # make sure all required properties have a value if not self.templateDict is None: model = self.editRel.gridProps.model() numrows = model.rowCount() for row in range(0, numrows): relProp = [ model.item(row, PROPERTY).data(Qt.EditRole), model.item(row, DATATYPE).data(Qt.EditRole), model.item(row, VALUE).data(Qt.EditRole) ] if relProp[VALUE] == "Null": # the value is null so see if it is required for templateProp in self.templateDict["properties"]: if templateProp[PROPERTY] == relProp[PROPERTY]: if templateProp[PROPREQ] == Qt.Checked: # this property must have a value self.helper.displayErrMsg( "Validate", "The property {} is required. Please enter a value." .format(relProp[PROPERTY])) return False return True @pyqtSlot() def on_btnClose_clicked(self): """ User Clicks the close button so close the dialog """ QDialog.accept(self)