labkeyBrowser.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580
  1. import os
  2. import unittest
  3. from __main__ import vtk, qt, ctk, slicer
  4. from slicer.ScriptedLoadableModule import *
  5. import slicerNetwork
  6. import loadDicom
  7. import json
  8. import zipfile
  9. import pathlib
  10. #
  11. # labkeySlicerPythonExtension
  12. #
  13. class labkeyBrowser(ScriptedLoadableModule):
  14. """Uses ScriptedLoadableModule base class, available at:
  15. https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
  16. """
  17. def __init__(self, parent):
  18. ScriptedLoadableModule.__init__(self, parent)
  19. self.parent.title = "labkeyBrowser" # TODO make this more human readable by adding spaces
  20. self.parent.categories = ["LabKey"]
  21. self.parent.dependencies = []
  22. self.parent.contributors = ["Andrej Studen (UL/FMF)"] # replace with "Firstname Lastname (Organization)"
  23. self.parent.helpText = """
  24. Interface to files in LabKey
  25. """
  26. self.parent.acknowledgementText = """
  27. Developed within the medical physics research programme of the Slovenian research agency.
  28. """ # replace with organization, grant and thanks.
  29. #
  30. # labkeySlicerPythonExtensionWidget
  31. #
  32. class labkeyBrowserWidget(ScriptedLoadableModuleWidget):
  33. """Uses ScriptedLoadableModuleWidget base class, available at:
  34. https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
  35. """
  36. def setup(self):
  37. ScriptedLoadableModuleWidget.setup(self)
  38. # Instantiate and connect widgets ...
  39. self.logic=labkeyBrowserLogic(self)
  40. self.network=slicerNetwork.labkeyURIHandler()
  41. #
  42. # Parameters Area
  43. #
  44. connectionCollapsibleButton = ctk.ctkCollapsibleButton()
  45. connectionCollapsibleButton.text = "Connection"
  46. self.layout.addWidget(connectionCollapsibleButton)
  47. connectionFormLayout = qt.QFormLayout(connectionCollapsibleButton)
  48. self.configDir=os.path.join(os.path.expanduser('~'),".labkey")
  49. self.serverURL=qt.QLineEdit("https://merlin.fmf.uni-lj.si")
  50. #self.serverURL.textChanged.connect(self.updateServerURL)
  51. connectionFormLayout.addRow("Server: ", self.serverURL)
  52. #copy initial setting
  53. #self.updateServerURL(self.serverURL.text);
  54. self.startDir=os.path.join(os.path.expanduser('~'),"temp","crt")
  55. self.userCertButton=qt.QPushButton("Load")
  56. self.userCertButton.toolTip="Load user certificate (crt)"
  57. self.userCertButton.connect('clicked(bool)',self.onUserCertButtonClicked)
  58. connectionFormLayout.addRow("User certificate:",self.userCertButton)
  59. self.privateKeyButton=qt.QPushButton("Load")
  60. self.privateKeyButton.toolTip="Load private key"
  61. self.privateKeyButton.connect('clicked(bool)',\
  62. self.onPrivateKeyButtonClicked)
  63. connectionFormLayout.addRow("Private key:",self.privateKeyButton)
  64. self.caCertButton=qt.QPushButton("Load")
  65. self.caCertButton.toolTip="Load CA certificate (crt)"
  66. self.caCertButton.connect('clicked(bool)',self.onCaCertButtonClicked)
  67. connectionFormLayout.addRow("CA certificate:",self.caCertButton)
  68. self.loadConfigButton=qt.QPushButton("Load configuration")
  69. self.loadConfigButton.toolTip="Load configuration"
  70. self.loadConfigButton.connect('clicked(bool)',\
  71. self.onLoadConfigButtonClicked)
  72. connectionFormLayout.addRow("Configuration:",self.loadConfigButton)
  73. self.loadZipButton=qt.QPushButton("Load certificates (.zip)")
  74. self.loadZipButton.toolTip="Load certificates from zip file"
  75. self.loadZipButton.connect('clicked(bool)',\
  76. self.onLoadZipButtonClicked)
  77. connectionFormLayout.addRow("Load certificates (merlin):",\
  78. self.loadZipButton)
  79. self.saveConfigButton=qt.QPushButton("Save configuration")
  80. self.saveConfigButton.toolTip="Save configuration"
  81. self.saveConfigButton.clicked.connect(self.onSaveConfigButtonClicked)
  82. connectionFormLayout.addRow("Configuration:",self.saveConfigButton)
  83. self.resetConfigButton=qt.QPushButton("Reset configuration")
  84. self.resetConfigButton.toolTip="Reset configuration"
  85. self.resetConfigButton.clicked.connect(self.onResetConfigButtonClicked)
  86. connectionFormLayout.addRow("Configuration:",self.resetConfigButton)
  87. self.initButton=qt.QPushButton("Init")
  88. self.initButton.toolTip="Initialize connection to the server"
  89. self.initButton.connect('clicked(bool)',self.onInitButtonClicked)
  90. connectionFormLayout.addRow("Connection:",self.initButton)
  91. self.authName=qt.QLineEdit("email")
  92. #self.authName.textChanged.connect(self.updateAuthName)
  93. connectionFormLayout.addRow("Labkey username: ", self.authName)
  94. self.authPass=qt.QLineEdit("")
  95. self.authPass.setEchoMode(qt.QLineEdit.Password)
  96. #self.authPass.textChanged.connect(self.updateAuthPass)
  97. connectionFormLayout.addRow("Labkey password: ", self.authPass)
  98. fileDialogCollapsibleButton = ctk.ctkCollapsibleButton()
  99. fileDialogCollapsibleButton.text = "Remote files"
  100. self.layout.addWidget(fileDialogCollapsibleButton)
  101. # Layout within the dummy collapsible button
  102. fileDialogFormLayout = qt.QFormLayout(fileDialogCollapsibleButton)
  103. #add item list for each found file/directory
  104. self.fileList=qt.QListWidget()
  105. self.fileList.toolTip="Select remote file"
  106. self.fileList.itemDoubleClicked.connect(self.onFileListDoubleClicked)
  107. self.currentRemoteDir=''
  108. #add dummy entry
  109. items=('.','..')
  110. self.populateFileList(items)
  111. fileDialogFormLayout.addWidget(self.fileList)
  112. #add selected file display
  113. self.selectedFile=qt.QLineEdit("")
  114. self.selectedFile.toolTip="Selected file"
  115. fileDialogFormLayout.addRow("Selected file :",self.selectedFile)
  116. #add possible file Content
  117. self.fileTypeSelector=qt.QComboBox()
  118. self.fileTypeSelector.toolTip="Select file type"
  119. items=self.network.fileTypesAvailable()
  120. self.populateFileTypeSelector(items)
  121. fileDialogFormLayout.addRow("File type :",self.fileTypeSelector)
  122. self.keepCachedFileCheckBox=qt.QCheckBox("keep cached file")
  123. self.keepCachedFileCheckBox.toolTip="Toggle local storage of labkey files"
  124. self.keepCachedFileCheckBox.setChecked(True)
  125. fileDialogFormLayout.addRow("Manage cache :",self.keepCachedFileCheckBox)
  126. loadFileButton=qt.QPushButton("Load file")
  127. loadFileButton.toolTip="Load file"
  128. loadFileButton.clicked.connect(self.onLoadFileButtonClicked)
  129. fileDialogFormLayout.addRow("Action :",loadFileButton)
  130. loadDirButton=qt.QPushButton("Load directory")
  131. loadDirButton.toolTip="Load directory"
  132. loadDirButton.clicked.connect(self.onLoadDirButtonClicked)
  133. fileDialogFormLayout.addRow("Action :",loadDirButton)
  134. def populateFileList(self,items):
  135. self.fileList.clear()
  136. for it_text in items:
  137. item=qt.QListWidgetItem(self.fileList)
  138. item.setText(it_text)
  139. item.setIcon(qt.QIcon(it_text))
  140. def populateFileTypeSelector(self,items):
  141. for item in items:
  142. self.fileTypeSelector.addItem(item)
  143. def cleanup(self):
  144. pass
  145. def onSelect(self):
  146. self.applyButton.enabled = self.inputSelector.currentNode() and self.outputSelector.currentNode()
  147. def onApplyButton(self):
  148. #logic = labkeySlicerPythonExtensionLogic()
  149. #enableScreenshotsFlag = self.enableScreenshotsFlagCheckBox.checked
  150. #screenshotScaleFactor = int(self.screenshotScaleFactorSliderWidget.value)
  151. print("Run the algorithm")
  152. #logic.run(self.inputSelector.currentNode(), self.outputSelector.currentNode(), enableScreenshotsFlag,screenshotScaleFactor)
  153. def onUserCertButtonClicked(self):
  154. filename=qt.QFileDialog.getOpenFileName(None,'Open user certificate',
  155. self.startDir, '*.crt')
  156. #pwd=qt.QInputDialog.getText(None,'Certificate password',
  157. # 'Enter certificate password',qt.QLineEdit.Password)
  158. if not(filename) :
  159. print("No file selected")
  160. return
  161. f=qt.QFile(filename)
  162. if not (f.open(qt.QIODevice.ReadOnly)) :
  163. print("Could not open file")
  164. return
  165. certList=qt.QSslCertificate.fromPath(filename)
  166. if len(certList) < 1:
  167. print ("Troubles parsing {0}".format(filename))
  168. return
  169. self.logic.cert=qt.QSslCertificate(f)
  170. print("cert.isNull()={0}".format(self.logic.cert.isNull()))
  171. self.userCertButton.setText(filename)
  172. self.authName.setText(self.logic.cert.subjectInfo("emailAddress"))
  173. def onPrivateKeyButtonClicked(self):
  174. filename=qt.QFileDialog.getOpenFileName(None,'Open private key',
  175. self.startDir, '*.key')
  176. if not (filename) :
  177. print ("No file selected")
  178. return
  179. f=qt.QFile(filename)
  180. if not (f.open(qt.QIODevice.ReadOnly)) :
  181. print ("Could not open file")
  182. return
  183. self.pwd=qt.QInputDialog.getText(None,'Private key password',
  184. 'Enter key password',qt.QLineEdit.Password)
  185. self.logic.key=qt.QSslKey(f,qt.QSsl.Rsa,qt.QSsl.Pem,qt.QSsl.PrivateKey,
  186. str(self.pwd))
  187. self.privateKeyButton.setText(filename)
  188. def onCaCertButtonClicked(self):
  189. filename=qt.QFileDialog.getOpenFileName(None,'Open authority certificate',
  190. self.startDir, '*.crt')
  191. if not(filename) :
  192. print("No file selected")
  193. return
  194. f=qt.QFile(filename)
  195. if not (f.open(qt.QIODevice.ReadOnly)) :
  196. print("Could not open file")
  197. return
  198. certList=qt.QSslCertificate.fromPath(filename)
  199. if len(certList) < 1:
  200. print("Troubles parsing {0}".format(filename))
  201. return
  202. self.logic.caCert=qt.QSslCertificate(f)#certList[0]
  203. self.caCertButton.setText(filename)
  204. def onLoadConfigButtonClicked(self):
  205. filename=qt.QFileDialog.getOpenFileName(None,'Open configuration file (JSON)',
  206. self.configDir, '*.json')
  207. self.network.parseConfig(filename,self)
  208. #self.serverURL.setText(self.network.hostname)
  209. #self.authName.setText(self.network.auth_name)
  210. #self.authPass.setText(self.network.auth_pass)
  211. self.loadConfigButton.setText(os.path.basename(filename))
  212. def onLoadZipButtonClicked(self):
  213. filename=qt.QFileDialog.getOpenFileName(\
  214. None,'Open certificate file (zip)',
  215. self.configDir, '*.zip')
  216. zpath=pathlib.Path(filename)
  217. user=zpath.stem
  218. userDir=os.path.join(os.path.expanduser('~'),'.labkey',user)
  219. if not os.path.isdir(userDir):
  220. os.mkdir(userDir)
  221. caName=None
  222. with zipfile.ZipFile(filename,'r') as zipObj:
  223. zipObj.extract(user+'.crt',userDir)
  224. zipObj.extract(user+'.key',userDir)
  225. zipList=zipObj.namelist()
  226. for f in zipList:
  227. if f.find('CA')>-1:
  228. caName=f
  229. zipObj.extract(f,userDir)
  230. self.serverURL.setText('https://merlin.fmf.uni-lj.si')
  231. self.privateKeyButton.setText(os.path.join(userDir,user+'.key'))
  232. self.userCertButton.setText(os.path.join(userDir,user+'.crt'))
  233. self.pwd='notUsed'
  234. self.caCertButton.setText(os.path.join(userDir,caName))
  235. self.authName.setText("guest")
  236. self.authPass.setText("guest")
  237. def onSaveConfigButtonClicked(self):
  238. connectionConfig=self.generateConfig()
  239. fhome=os.path.expanduser('~')
  240. labkeyDir=os.path.join(fhome,".labkey")
  241. if not os.path.isdir(labkeyDir):
  242. os.mkdir(labkeyDir)
  243. fconfig=os.path.join(labkeyDir,'network.json')
  244. with open(fconfig,'w') as f:
  245. json.dump(connectionConfig,f,indent=3)
  246. print("Done")
  247. def generateConfig(self):
  248. connectionConfig={}
  249. #setup SSL
  250. if self.privateKeyButton.text=="Load" or\
  251. self.userCertButton.text=="Load" or\
  252. self.caCertButton.text=="Load":
  253. #no ssl
  254. pass
  255. else:
  256. sslSetup={}
  257. sslSetup['user']=self.userCertButton.text
  258. sslSetup['key']=self.privateKeyButton.text
  259. sslSetup['ca']=self.caCertButton.text
  260. try:
  261. sslSetup['keyPwd']=self.pwd
  262. except AttributeError:
  263. sslSetup['keyPwd']='notUsed'
  264. connectionConfig['SSL']=sslSetup
  265. connectionConfig["host"]=self.serverURL.text
  266. connectionConfig["context"]="labkey"
  267. labkeySetup={}
  268. labkeySetup['user']=self.authName.text
  269. labkeySetup['password']=self.authPass.text
  270. connectionConfig['labkey']=labkeySetup
  271. orthancSetup={}
  272. if self.serverURL.text.find('merlin')>-1:
  273. orthancSetup['server']='https://orthanc.fmf.uni-lj.si'
  274. if self.serverURL.text.find('onko-nix')>-1:
  275. orthancSetup['server']='http://onko-nix.onko-i.si:8042'
  276. orthancSetup['user']='ask'
  277. orthancSetup['password']='askPassword'
  278. connectionConfig['orthanc']=orthancSetup
  279. return connectionConfig
  280. def onResetConfigButtonClicked(self):
  281. self.serverURL.setText('URL')
  282. self.privateKeyButton.setText("Load")
  283. self.userCertButton.setText("Load")
  284. self.caCertButton.setText("Load")
  285. self.authName.setText("labkey username")
  286. self.authPass.setText("password")
  287. def onInitButtonClicked(self):
  288. self.network.connectionConfig=self.generateConfig()
  289. self.network.initRemote()
  290. if self.network.getCSRF()==None:
  291. self.initButton.setStyleSheet("background-color: red")
  292. else:
  293. self.initButton.setStyleSheet("background-color: green")
  294. #def updateAuthName(self,txt):
  295. #self.network.auth_name=txt
  296. #print("Setting username to {0}".format(self.network.auth_name))
  297. #def updateAuthPass(self,txt):
  298. #self.network.auth_pass=txt
  299. #print("Setting password.")
  300. #def updateServerURL(self,txt):
  301. #self.network.hostname=txt
  302. #print("Setting hostname to {}".format(self.network.hostname))
  303. def onFileListDoubleClicked(self,item):
  304. if item == None:
  305. print("Selected items: None")
  306. return
  307. iText=item.text()
  308. print("Selected items: {0} ".format(iText))
  309. #this is hard -> compose path string from currentRemoteDir and selection
  310. if item.text().find('..')==0:
  311. #one up
  312. idx=self.currentRemoteDir.rfind('/')
  313. if idx<0:
  314. self.currentRemoteDir=''
  315. else:
  316. self.currentRemoteDir=self.currentRemoteDir[:idx]
  317. elif item.text().find('.')==0:
  318. pass
  319. else:
  320. if len(self.currentRemoteDir)>0:
  321. self.currentRemoteDir+='/'
  322. self.currentRemoteDir+=item.text()
  323. print("Listing {0}".format(self.currentRemoteDir))
  324. flist=self.network.toRelativePath(
  325. self.network.listRelativeDir(self.currentRemoteDir))
  326. print("Got")
  327. print(flist)
  328. flist.insert(0,'..')
  329. flist.insert(0,'.')
  330. self.populateFileList(flist)
  331. self.selectedFile.setText(self.currentRemoteDir)
  332. def onLoadFileButtonClicked(self):
  333. properties={}
  334. localPath=self.network.DownloadFileToCache(self.selectedFile.text)
  335. slicer.util.loadNodeFromFile(localPath,self.fileTypeSelector.currentText,
  336. properties,returnNode=False)
  337. if not self.keepCachedFileCheckBox.isChecked():
  338. os.remove(localPath)
  339. def onLoadDirButtonClicked(self):
  340. #dir=self.network.loadDir(self.selectedFile.text)
  341. dir=self.selectedFile.text
  342. try:
  343. self.loadDicomLogic.load(self.network,dir)
  344. except:
  345. self.loadDicomLogic=loadDicom.loadDicomLogic(self)
  346. self.loadDicomLogic.load(self.network,dir)
  347. # labkeySlicerPythonExtensionLogic
  348. #
  349. class labkeyBrowserLogic(ScriptedLoadableModuleLogic):
  350. """This class should implement all the actual
  351. computation done by your module. The interface
  352. should be such that other python code can import
  353. this class and make use of the functionality without
  354. requiring an instance of the Widget.
  355. Uses ScriptedLoadableModuleLogic base class, available at:
  356. https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
  357. """
  358. def __init__(self,parent):
  359. ScriptedLoadableModuleLogic.__init__(self, parent)
  360. self.qnam=qt.QNetworkAccessManager()
  361. def hasImageData(self,volumeNode):
  362. """This is a dummy logic method that
  363. returns true if the passed in volume
  364. node has valid image data
  365. """
  366. if not volumeNode:
  367. print('no volume node')
  368. return False
  369. if volumeNode.GetImageData() == None:
  370. print('no image data')
  371. return False
  372. return True
  373. def takeScreenshot(self,name,description,type=-1):
  374. # show the message even if not taking a screen shot
  375. self.delayDisplay(description)
  376. if self.enableScreenshots == 0:
  377. return
  378. lm = slicer.app.layoutManager()
  379. # switch on the type to get the requested window
  380. widget = 0
  381. if type == slicer.qMRMLScreenShotDialog.FullLayout:
  382. # full layout
  383. widget = lm.viewport()
  384. elif type == slicer.qMRMLScreenShotDialog.ThreeD:
  385. # just the 3D window
  386. widget = lm.threeDWidget(0).threeDView()
  387. elif type == slicer.qMRMLScreenShotDialog.Red:
  388. # red slice window
  389. widget = lm.sliceWidget("Red")
  390. elif type == slicer.qMRMLScreenShotDialog.Yellow:
  391. # yellow slice window
  392. widget = lm.sliceWidget("Yellow")
  393. elif type == slicer.qMRMLScreenShotDialog.Green:
  394. # green slice window
  395. widget = lm.sliceWidget("Green")
  396. else:
  397. # default to using the full window
  398. widget = slicer.util.mainWindow()
  399. # reset the type so that the node is set correctly
  400. type = slicer.qMRMLScreenShotDialog.FullLayout
  401. # grab and convert to vtk image data
  402. qpixMap = qt.QPixmap().grabWidget(widget)
  403. qimage = qpixMap.toImage()
  404. imageData = vtk.vtkImageData()
  405. slicer.qMRMLUtils().qImageToVtkImageData(qimage,imageData)
  406. annotationLogic = slicer.modules.annotations.logic()
  407. annotationLogic.CreateSnapShot(name, description, type, self.screenshotScaleFactor, imageData)
  408. def run(self,inputVolume,outputVolume,enableScreenshots=0,screenshotScaleFactor=1):
  409. """
  410. Run the actual algorithm
  411. """
  412. self.delayDisplay('Running the aglorithm')
  413. self.enableScreenshots = enableScreenshots
  414. self.screenshotScaleFactor = screenshotScaleFactor
  415. self.takeScreenshot('labkeySlicerPythonExtension-Start','Start',-1)
  416. return True
  417. class labkeyBrowserTest(ScriptedLoadableModuleTest):
  418. """
  419. This is the test case for your scripted module.
  420. Uses ScriptedLoadableModuleTest base class, available at:
  421. https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
  422. """
  423. def setUp(self):
  424. """ Do whatever is needed to reset the state - typically a scene clear will be enough.
  425. """
  426. slicer.mrmlScene.Clear(0)
  427. def runTest(self):
  428. """Run as few or as many tests as needed here.
  429. """
  430. self.setUp()
  431. self.test_labkeySlicerPythonExtension1()
  432. def test_labkeyBrowser(self):
  433. """ Ideally you should have several levels of tests. At the lowest level
  434. tests sould exercise the functionality of the logic with different inputs
  435. (both valid and invalid). At higher levels your tests should emulate the
  436. way the user would interact with your code and confirm that it still works
  437. the way you intended.
  438. One of the most important features of the tests is that it should alert other
  439. developers when their changes will have an impact on the behavior of your
  440. module. For example, if a developer removes a feature that you depend on,
  441. your test should break so they know that the feature is needed.
  442. """
  443. self.delayDisplay("Starting the test")
  444. #
  445. # first, get some data
  446. #
  447. import urllib
  448. downloads = (
  449. ('http://slicer.kitware.com/midas3/download?items=5767', 'FA.nrrd', slicer.util.loadVolume),
  450. )
  451. for url,name,loader in downloads:
  452. filePath = slicer.app.temporaryPath + '/' + name
  453. if not os.path.exists(filePath) or os.stat(filePath).st_size == 0:
  454. print('Requesting download %s from %s...\n' % (name, url))
  455. urllib.urlretrieve(url, filePath)
  456. if loader:
  457. print('Loading %s...\n' % (name,))
  458. loader(filePath)
  459. self.delayDisplay('Finished with download and loading\n')
  460. volumeNode = slicer.util.getNode(pattern="FA")
  461. logic = labkeySlicerPythonExtensionLogic()
  462. self.assertTrue( logic.hasImageData(volumeNode) )
  463. self.delayDisplay('Test passed!')
  464. def getHomeDir():
  465. try:
  466. return os.environ['HOME']
  467. except:
  468. fhome=os.environ['HOMEDRIVE']+os.environ['HOMEPATH']
  469. return fhome