Open Popup from a Popup

I am working on a popup that will insert data into a database. However prior to storing the data, I would like the script to ensure that all the required fields are filled, and if so the user will need to validate their credentials to prior to the data being inserted into the database. However, I am having trouble with the system.perspective.openPopup function. When I run the script I get the following error on the openPopup line: AttributeError: 'com.inductiveautomation.ignition.designer.gui.tool' object has no attribute 'perspective'

def runAction(self, event):
	UDT = self.parent.parent.getChild("UDTFlex").getChild("UDT").props.value
	Asset = self.parent.parent.getChild("AssetIDFlex").getChild("Asset").props.value
	Users = self.parent.parent.getChild("OperatorsFlex").getChild("Users").props.value
	Supervisor = self.session.props.auth.user.userName
	Campaign = self.parent.parent.getChild("CampaignID").props.text
	Batchid = self.parent.parent.getChild("BatchID").props.text
	startTime = system.date.now()
	
	AssetS = str(Asset)
	UDTS = str(UDT)
	OperatorsS = str(Operators)
	SupervisorS = str(Supervisor)
	CampaignS = str(Campaign)
	BatchidS = str(Batchid)
	startTimeS = systeme.date.format(startTime, "yyyy-MM-dd HH:mm:ss")
	
	if UDTS == "" or AssetS == "" or OperatorsS == "":
		system.perspective.openPopup('empfieldsPopup', 'empfieldsPopup', showCloseIcon = True, pageID = 'addUDTpopup')
	else:
		system.perspective.openPopup('authPopup', 'authPopup')
	
	query = "INSERT INTO udtSelect (Asset, UDT, Operators, Supervisor, Campaign, Batchid, startTime) Values(?,?,?,?,?,?,?)"
	args = [AssetS, UDTS, OperatorsS, SupervisorS, CampaignS, BatchidS, startTimeS]
	system.db.runPrepUpdate(query,args)
	system.perspective.sendMessage("refresh", "session")
	system.perspective.closePopup('addUDTpopup')

This script is configured on the submit button of the currently opened popup

EDIT: I got the popups to open correctly. There were some minor typos and references preventing the script to run up to the if statement.

However, I want the script to update the database only after the authentication is successful, but I am unsure how to refer to the result of the authPopup into this script.

Put a message handler on the root of the popup, and pass a message to that handler from the authentication popup, with the auth status in the payload.
Grab all your required data and perform the database inserts from this handler instead of the button script.

Or get rid of the popups and use system.perspective.authenticationChallenge - Ignition User Manual 8.1 - Ignition Documentation

I rewrote the script on the button to invoke and authentication challenge as shown below:

def runAction(self, event):
	UDT = self.parent.parent.getChild("UDTFlex").getChild("UDT").props.value
	Asset = self.parent.parent.getChild("AssetIDFlex").getChild("Asset").props.value
	Users = self.parent.parent.getChild("OperatorsFlex").getChild("Users").props.value
	Supervisor = self.session.props.auth.user.userName
	Campaign = self.parent.parent.getChild("CampaignID").props.text
	Batchid = self.parent.parent.getChild("BatchID").props.text
	startTime = system.date.now()
	
	AssetS = str(Asset)
	UDTS = str(UDT)
	UsersS = 'Kawsar' ##str(Users)
	SupervisorS = str(Supervisor)
	CampaignS = str(Campaign)
	BatchidS = str(Batchid)
	startTimeS = system.date.format(startTime, "yyyy-MM-dd HH:mm:ss")
	
	if UDTS == "" or AssetS == "": ## or UsersS == "":
		system.perspective.openPopup('empfieldsPopup', 'empfieldsPopup', showCloseIcon = True)
	else:
		system.perspective.authenticationChallenge(timeout = 5, payload = {"isAction":"true"})

I then configured the following on the session events for the authentication challenge:

def onAuthChallengeCompleted(session, payload, result):
	if result.isSuccess():
		system.perspective.sendMessage('loginSuccess', 'session')
	else:
		system.perspective.openPopup('incpassPopup', 'incpassPopup')

I then created a message handler on the root of the popup called "loginSuccess":

UDT = self.parent.parent.getChild("UDTFlex").getChild("UDT").props.value
	Asset = self.parent.parent.getChild("AssetIDFlex").getChild("Asset").props.value
	Users = self.parent.parent.getChild("OperatorsFlex").getChild("Users").props.value
	Supervisor = self.session.props.auth.user.userName
	Campaign = self.parent.parent.getChild("CampaignID").props.text
	Batchid = self.parent.parent.getChild("BatchID").props.text
	startTime = system.date.now()
		
	AssetS = str(Asset)
	UDTS = str(UDT)
	UsersS = "Kawsar" ##str(Users)
	SupervisorS = str(Supervisor)
	CampaignS = str(Campaign)
	BatchidS = str(Batchid)
	startTimeS = system.date.format(startTime, "yyyy-MM-dd HH:mm:ss")
		
	query = "INSERT INTO udtSelect (Asset, UDT, Operators, Supervisor, Campaign, Batchid, startTime) Values(?,?,?,?,?,?,?)"
	args = [AssetS, UDTS, OperatorsS, SupervisorS, CampaignS, BatchidS, startTimeS]
	system.db.runPrepUpdate(query,args)
	system.perspective.sendMessage("refresh", "session")
	system.perspective.closePopup('addUDTpopup')

This message handler should retrieve the values from the popup again and store said values into the database, which should then send a message to the table on the main page to refresh the table with the new database values.

I am now running into the following error on the session events: com.inductiveautomation.ignition.common.script.JythonExecException: Traceback (most recent call last): File "", line 3, in onAuthChallengeCompleted at com.inductiveautomation.perspective.gateway.script.AbstractScriptingFunctions.lambda$operateOnPage$0(AbstractScriptingFunctions.java:64) at com.inductiveautomation.perspective.gateway.script.AbstractScriptingFunctions.operateOnSession(AbstractScriptingFunctions.java:120) at com.inductiveautomation.perspective.gateway.script.AbstractScriptingFunctions.operateOnPage(AbstractScriptingFunctions.java:47) at com.inductiveautomation.perspective.gateway.script.PerspectiveScriptingFunctions.sendMessage(PerspectiveScriptingFunctions.java:749) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(Unknown Source) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source) at java.base/java.lang.reflect.Method.invoke(Unknown Source) java.lang.IllegalArgumentException: java.lang.IllegalArgumentException: No perspective page attached to this thread

I believe this is saying that the sendMessage function is unable to determine which page to send the message so it therefore requires a pageID, but I am not sure how to pass the pageID of a popup. Should the payload of the authentication challenge on the button be the pageID of the popup? Do popups even have pageIDs since they are not configured pages with URLs?

disclaimer: I've never used the auth challenge.
But mixing it up with popups looks to me like a snake pit.

Hey @Kawsar_Ahmed

If MH is on the root of the popup, then ideally the scope of the message handler should be page level, but system.perspective.authenticationChallenge opens a new tab, so the trigger will not have the access to the popup as it is a session level trigger.

One quick patch, will be to bind these values to Session Custom properties, then set the MH in Project > Session Events > Message, and this MH will have access to the Session object. And now the session object has custom properties which are bound with the page level components and their values.

It will be worth a try.

So I have configured it to trigger it an authentication event which sends a system level message which then sends a message to the popup to store the data into the database as shown below. However I am now getting a "Websocket disconnected from the session" error.

Script on button to trigger authentication challenge:

def runAction(self, event):
	UDT = self.parent.parent.getChild("UDTFlex").getChild("UDT").props.value
	Asset = self.parent.parent.getChild("AssetIDFlex").getChild("Asset").props.value
	Users = self.parent.parent.getChild("OperatorsFlex").getChild("Users").props.value
	Supervisor = self.session.props.auth.user.userName
	Campaign = self.parent.parent.getChild("CampaignID").props.text
	Batchid = self.parent.parent.getChild("BatchID").props.text
	startTime = system.date.now()
	
	AssetS = str(Asset)
	UDTS = str(UDT)
	UsersS = 'Kawsar' ##str(Users)
	SupervisorS = str(Supervisor)
	CampaignS = str(Campaign)
	BatchidS = str(Batchid)
	startTimeS = system.date.format(startTime, "yyyy-MM-dd HH:mm:ss")
	
	if UDTS == "" or AssetS == "": ## or UsersS == "":
		system.perspective.openPopup('empfieldsPopup', 'empfieldsPopup', showCloseIcon = True)
	else:
		system.perspective.authenticationChallenge(timeout = 5, payload = {'pageID':self.page.id})

Authentication Challenge Session Event:

def onAuthChallengeCompleted(session, payload, result):
	if result.isSuccess():
		system.perspective.sendMessage('loginSuccess', scope = 'session')
	else:
		system.perspective.openPopup('incpassPopup', 'incpassPopup')

Message on session event:

def handleMessage(session, payload):
	system.perspective.sendMessage('storeData', scope = 'session')

Message on popup root container to store data:

def onMessageReceived(self, payload):
	UDT = self.parent.parent.getChild("UDTFlex").getChild("UDT").props.value
	Asset = self.parent.parent.getChild("AssetIDFlex").getChild("Asset").props.value
	Users = self.parent.parent.getChild("OperatorsFlex").getChild("Users").props.value
	Supervisor = self.session.props.auth.user.userName
	Campaign = self.parent.parent.getChild("CampaignID").props.text
	Batchid = self.parent.parent.getChild("BatchID").props.text
	startTime = system.date.now()
		
	AssetS = str(Asset)
	UDTS = str(UDT)
	UsersS = "Kawsar" ##str(Users)
	SupervisorS = str(Supervisor)
	CampaignS = str(Campaign)
	BatchidS = str(Batchid)
	startTimeS = system.date.format(startTime, "yyyy-MM-dd HH:mm:ss")
		
	query = "INSERT INTO udtSelect (Asset, UDT, Operators, Supervisor, Campaign, Batchid, startTime) Values(?,?,?,?,?,?,?)"
	args = [AssetS, UDTS, OperatorsS, SupervisorS, CampaignS, BatchidS, startTimeS]
	system.db.runPrepUpdate(query,args)
	system.perspective.sendMessage("refresh", "session")
	system.perspective.closePopup('addUDTpopup')

Also, this method of bouncing messages around seems to be a little extreme to me, there must be a more concise method to this correct?

Pull the pageID from the popup with self.page.props.pageId (you have self.page.id?). There are some optional arguments for system.perspective.sendMessage, pageId being one of them. The message send call in the Auth Event handler should be system.perspective.sendMessage('storeData', scope='session', pageId=payload['pageID'])

After that, change the message handler on the popup root container to listen at session level.

Mildly off topic but I would recommend putting a custom object called 'data' in the root container's custom properties(or the view custom properties), and have the items 'UDT', 'Asset', 'Users', 'Campaign', and 'Batchid' in it. Bidirectionally bind the associated entry fields to their respective keys in this object.

Then when you go to pull the data (or pass it in a payload) you can just pull self.view.getChild("root").custom.data and pass that wherever or assign it to a variable in the script. It would also allow you to use a custom boolean prop okayToSubmit with a binding that checks if UDT or Asset is blank. You would then only have to check this boolean in your button event script, turning it into a 4 line script.

I have implemented this, but for some reason I am getting a "websocket disconnected from session" error every time I attempt to execute. Also, how would this work with additional auth challenges on other pages? Would I just make all the message handlers have the same name and use the pageID to ensure the message is sent to the correct page?

The websocket disconnect is strange, I would recommend that you reach out to support with that, there might be something else interfering . Are you getting that error when you are testing in the designer or in a normal perspective session?

Since you are having to pass a pageID to be able to send a message back to the page that initiated the challenge, you can probably get away with relying on a handler on each page with the expected logic in it for a successful auth challenge.

If you are going to have multiple things on the same page calling an auth challenge with different expected process/data flows, you will want to add a 'sourceID' key to your payload as a way of identifying what called the auth challenge, and then have your page based handlers check that to determine what to do with the passed data.

At some point it will get cumbersome to have handlers on every page, so I would recommend having all your data handling logic in the auth challenge handler itself, and then have that report back to the page that called it (either opening a success popup or something similar and closing the original popup). If there is a lot of data processing or other logic, put it into a project level function and call that from the auth challenge handler.

I finally solved the issue. At first I thought the problem was that triggering the authentication challenge as scripted above was causing the page to reload, which led to a loss of the values I wanted to store. However, I tried to alleviate this by writing the values to memory tag prior to the authentication challenge, and upon success of the challenge the message handler on the root container would retrieve the values from the memory tags and would process them as needed. However this did not work. The values would write to the memory tags prior to the auth challenge, but the auth challenge failed to execute the message handler upon success.

I found that the issue has something to do with the auth challenge reloading the page maybe disconnecting the session for a second and terminating the auth challenge prematurely, hence the websocket error mentioned above. However, I cannot be certain that this is the root cause.

However I did find that having the popup open as an embedded frame did not reload the session and worked at completing the sequence of events I desired. See code and picture below.

system.perspective.authenticationChallenge(timeout = 5, framing = "Embedded", payload = {'pageID':self.page.props.pageId})