Commit 9d2187ff authored by sveseli's avatar sveseli
Browse files

Merged 1.9.3

parents aa57a7ae 5f464dd6
Release 1.12 (09/15/2018)
=============================
- Added generic script processing plugin for DAQ service
- Parallelized cataloging plugin; this significantly increased performance
for uploads using directory mode
Release 1.11 (05/15/2018)
=============================
- Added support for compressing and decompressing files
......
......@@ -17,5 +17,5 @@ DM_CAT_WEB_SERVICE_HOST=DM_HOSTNAME
DM_CAT_WEB_SERVICE_PORT=44436
DM_PROC_WEB_SERVICE_HOST=DM_HOSTNAME
DM_PROC_WEB_SERVICE_PORT=55536
DM_SOFTWARE_VERSION=1.11
DM_SOFTWARE_VERSION=1.12
......@@ -280,6 +280,8 @@ class AddExperimentTab(QWidget):
# Toggles which table is visible
def toggleDetails(self):
if self.showingDetails == 1:
self.detailsTable.clearFocus()
self.detailsTable.clearSelection()
self.tableWidget.show()
self.detailsTable.hide()
self.detailBtn.show()
......@@ -291,9 +293,11 @@ class AddExperimentTab(QWidget):
except AttributeError:
self.selectProposalDialog()
return
self.tableWidget.hide()
self.proposalDetails()
self.detailsTable.show()
self.tableWidget.setSelectionMode(QAbstractItemView.NoSelection)
self.tableWidget.hide()
self.tableWidget.setSelectionMode(QAbstractItemView.SingleSelection)
self.detailBtn.hide()
self.showingDetails = 1
self.propToggle.setEnabled(False)
......@@ -302,6 +306,7 @@ class AddExperimentTab(QWidget):
def proposalDetails(self):
QApplication.setOverrideCursor(Qt.WaitCursor)
id = self.tableWidget.item(self.tableWidget.currentRow(), 0).data(Qt.UserRole).toInt()[0]
self.tableWidget.clearFocus()
if str(self.runDropdown.currentText()) == 'Current Run' or str(self.runDropdown.currentText()) == 'Current Year':
if self.parent.useEsaf == 1:
proposal = self.esafPropApi.getEsafById(id)
......@@ -371,6 +376,11 @@ class AddExperimentTab(QWidget):
self.startBtn.setEnabled(True)
self.detailBtn.setEnabled(True)
# Returns the tables on this tab
def getTables(self):
tables = [self.tableWidget, self.detailsTable]
return tables
# Checks to see if the back button should send the user out of the detail table or back a tab
def checkBack(self):
if self.showingDetails == 1:
......
#!/usr/bin/env python
from PyQt4.QtGui import QAction
from PyQt4.QtGui import QCursor
from PyQt4.QtGui import QGridLayout, QSpacerItem, QSizePolicy, QPushButton, QWidget, \
QFont, QLabel, QTableWidget, QTableWidgetItem, QColor, QAbstractItemView, QHBoxLayout, QPalette, QMessageBox
from PyQt4.QtCore import Qt
......@@ -61,10 +62,10 @@ class DaqsTab(QWidget):
grid.addWidget(self.detailsTable, 3, 0, 1, 5)
grid.addWidget(self.daqTable, 3, 0, 1, 5)
addBtn = QPushButton('Start New', self)
addBtn.setFocusPolicy(Qt.NoFocus)
addBtn.clicked.connect(lambda: self.parent.tabs.setCurrentIndex(0))
addBtn.setMaximumWidth(130)
self.addBtn = QPushButton('Start New', self)
self.addBtn.setFocusPolicy(Qt.NoFocus)
self.addBtn.clicked.connect(lambda: self.parent.tabs.setCurrentIndex(0))
self.addBtn.setMaximumWidth(130)
self.detailBtn = QPushButton('Show Details', self)
self.detailBtn.setFocusPolicy(Qt.NoFocus)
......@@ -82,7 +83,7 @@ class DaqsTab(QWidget):
self.stopBtn.setMaximumWidth(130)
hbox1 = QHBoxLayout()
hbox1.addWidget(addBtn)
hbox1.addWidget(self.addBtn)
hbox1.addWidget(self.detailBtn)
hbox1.addWidget(self.clearBtn)
hbox1.addWidget(self.stopBtn)
......@@ -204,27 +205,38 @@ class DaqsTab(QWidget):
# Toggles to show/hide the details table of the thing that is selected
def toggleDetails(self):
if self.showingDetails == 1:
self.refreshBtn.setEnabled(True)
self.detailsTable.clearFocus()
self.detailsTable.clearSelection()
self.refreshBtn.show()
self.checkFail()
self.daqTable.show()
self.detailsTable.hide()
self.detailBtn.show()
self.backBtn.hide()
self.addBtn.show()
self.clearBtn.show()
self.detailBtn.show()
self.stopBtn.show()
self.showingDetails = 0
else:
self.refreshBtn.setEnabled(False)
self.stopBtn.setEnabled(False)
self.clearBtn.setEnabled(False)
self.refreshBtn.hide()
self.daqTable.setSelectionMode(QAbstractItemView.NoSelection)
self.daqTable.hide()
self.daqTable.setSelectionMode(QAbstractItemView.SingleSelection)
self.daqDetails()
self.detailsTable.show()
self.detailBtn.hide()
self.backBtn.show()
self.addBtn.hide()
self.clearBtn.hide()
self.detailBtn.hide()
self.stopBtn.hide()
self.showingDetails = 1
def daqDetails(self):
self.detailsTable.setSortingEnabled(False)
id = self.daqTable.item(self.daqTable.currentRow(), 0).data(Qt.UserRole).toString()
self.daqTable.clearFocus()
allInfo = self.experimentDaqApi.getDaqInfo(id)
self.detailsTable.setRowCount(len(allInfo.data))
......@@ -270,9 +282,14 @@ class DaqsTab(QWidget):
else:
self.detailsTable.update(index)
# Returns the tables on this tab
def getTables(self):
tables = [self.daqTable, self.detailsTable]
return tables
# Signals the parent to handle the right click event
def contextMenuEvent(self, event):
if self.daqTable.isVisible():
self.parent.handleRightClickMenu(self.daqTable, event)
self.parent.handleRightClickMenu(self.daqTable, event, toggleDetailsAction=self.toggleDetails)
else:
self.parent.handleRightClickMenu(self.detailsTable, event)
......@@ -2,9 +2,14 @@
import sys
import os
from PyQt4.QtGui import QMainWindow, QStackedLayout, QApplication, QWidget, QTabWidget, QSplashScreen, QPixmap, QMenu, QAction, QCursor
from PyQt4.QtGui import QMainWindow, QStackedLayout, QApplication, QWidget, QTabWidget, QSplashScreen, QPixmap, QMenu, QAction, QCursor, QMessageBox, QAbstractButton
from PyQt4.QtCore import Qt, QTimer, QElapsedTimer
from time import sleep, time
import threading
import smtplib
from email.mime.text import MIMEText
import datetime
from traceback import format_tb
from dm.common.constants import dmStatus
from dm.common.exceptions.dmException import DmException
......@@ -22,8 +27,15 @@ from genParamsTab import GenParamsTab
from fileTab import FileTab
from workflowTab import WorkflowTab
#TODO: Make allUserTable sortable on FileTab
# Catches all potential unhandled exceptions
def dmExceptionHook(type, value, traceback):
temp = traceback.tb_frame.f_locals['self']
errorList = ['Error: ' + str(value) + '\n\n']
errorList = errorList + ['Traceback (most recent call last):\n']
errorList = errorList + format_tb(traceback)
errorStr = ''.join(errorList)
DmStationUi.unhandledDialog(window, temp, errorStr)
sys.exit(errorStr)
class DmStationUi(QMainWindow):
def __init__(self):
......@@ -42,6 +54,9 @@ class DmStationUi(QMainWindow):
self.setWindowTitle('DM Station: %s' % self.stationName)
self.setGeometry(0, 0, 800, 400)
# Email address that the notification emails will list they are sent from
self.dmEmail = 'dmNoReply@aps.anl.gov'
# Variable to hold the user instances
self.currentUsers = []
......@@ -80,6 +95,8 @@ class DmStationUi(QMainWindow):
if self.beamlineName or self.onlyEsaf == 1:
self.addExperimentTab = AddExperimentTab(self.stationName, self)
self.fileTab = FileTab(self.stationName, self)
# Hide until ready for release
self.workflowTab = WorkflowTab(self.stationName, self)
# Add the windows to the stack.
......@@ -97,6 +114,8 @@ class DmStationUi(QMainWindow):
self.tabs.addTab(self.stackedWidget, "Experiments")
self.tabs.addTab(self.daqsTab, "DAQs")
self.tabs.addTab(self.uploadsTab, "Uploads")
# Hide until ready for release
self.tabs.addTab(self.workflowTab, "Workflows")
# Set a central widget to hold everything
......@@ -117,6 +136,7 @@ class DmStationUi(QMainWindow):
self.useEsaf = self.configManager.getInstance().getStationUseEsaf()
self.beamlineManagers = self.configManager.getInstance().getBeamlineManagers()
self.username, self.password = self.configManager.parseLoginFile()
self.dmAdminEmail = self.configManager.getDmAdminEmail()
if self.useEsaf:
self.useEsaf = int(self.useEsaf)
if not self.stationName:
......@@ -164,8 +184,16 @@ class DmStationUi(QMainWindow):
return self.tabs.currentIndex()
# Used to change between tabs
def setTab(self, tab):
self.stackedLayout.setCurrentIndex(tab)
def setTab(self, stackIdx):
stack = self.stackedLayout.widget(stackIdx)
tables = stack.getTables()
for table in tables:
self.clearSelection(table)
self.stackedLayout.setCurrentIndex(stackIdx)
# Clears selections from table
def clearSelection(self, table):
table.clearSelection()
# Refreshes the experiments, DAQ, and Uploads tables.
def refreshTables(self):
......@@ -186,11 +214,17 @@ class DmStationUi(QMainWindow):
return indexes, selectedRows
# Capture the right click event and open a context menu
def handleRightClickMenu(self, table, event):
def handleRightClickMenu(self, table, event, toggleDetailsAction=None):
if table.selectionModel().selection().indexes():
index = table.selectionModel().selection().indexes()[0]
row, column = index.row(), index.column()
menu = QMenu(self)
if toggleDetailsAction:
showDetailsAction = QAction("Show Details", self)
showDetailsAction.triggered.connect(toggleDetailsAction)
menu.addAction(showDetailsAction)
copyAction = QAction("Copy", self)
copyAction.triggered.connect(lambda: self.copyAction(row, column, table))
menu.addAction(copyAction)
......@@ -203,10 +237,36 @@ class DmStationUi(QMainWindow):
sysclip = QApplication.clipboard()
sysclip.setText(clipboard)
def emailAdmin(self, text):
msg = MIMEText(text)
msg['Subject'] = self.stationName + " Error Report"
msg['From'] = self.dmEmail
COMMASPACE = ', '
msg['To'] = COMMASPACE.join(self.dmAdminEmail)
s = smtplib.SMTP('localhost')
s.sendmail(self.dmEmail, self.dmAdminEmail, msg.as_string())
s.quit()
def unhandledDialog(self, widget, error):
msg = QMessageBox()
msg.setIcon(QMessageBox.Critical)
msg.setText("Unexpected Error Occurred")
msg.setInformativeText("You encountered an error that has not been handled and the program must exit. Please click the 'Notify Admin' button and it will be fixed shortly.")
msg.setDetailedText(error)
msg.setWindowTitle("DM Unexpected Error")
notify = msg.addButton("Notify Admin", QMessageBox.YesRole)
msg.addButton("Quit", QMessageBox.NoRole)
msg.exec_()
if (msg.clickedButton() == notify):
self.emailAdmin(error)
if __name__ == "__main__":
try:
app = QApplication(sys.argv)
sys.excepthook = dmExceptionHook
start = time()
homeDir = os.path.dirname(sys.argv[0])
splashPic = QPixmap(homeDir + '/images/splash2.jpg')
......
......@@ -276,33 +276,77 @@ class dmStationUiTest(unittest.TestCase):
QTest.mouseClick(self.gui.daqsTab.clearBtn, Qt.LeftButton)
## Start the aborting of the upload
QTest.mouseClick(self.gui.uploadsTab.backBtn, Qt.LeftButton)
model = self.gui.uploadsTab.tableWidget.findChild(QAbstractTableModel)
for row in range(model.rowCount()):
if model.data(model.index(row, 0)).toString() == "DmStationUITestSuite":
waitAbort = True
while waitAbort:
for row in range(model.rowCount()):
if model.data(model.index(row, 0)).toString() == "DmStationUITestSuite":
if model.data(model.index(row, 1)).toString() == "done":
waitAbort = False
else:
QTest.mouseClick(self.gui.uploadsTab.refreshBtn, Qt.LeftButton)
QTest.qWait(1000)
colPos = self.gui.uploadsTab.tableWidget.columnViewportPosition(self.initialCol)
rowPos = self.gui.uploadsTab.tableWidget.rowViewportPosition(row)
QTest.mouseClick(self.gui.uploadsTab.tableWidget.viewport(), Qt.LeftButton, pos=QPoint(colPos + 1, rowPos + 1), delay=1000)
QTest.mouseClick(self.gui.uploadsTab.stopBtn, Qt.LeftButton)
QTest.mouseClick(self.gui.uploadsTab.backBtn, Qt.LeftButton)
self.gui.setTab(2)
self.gui.tabs.setCurrentIndex(0)
self.fileTab()
def fileTab(self):
# Test Initial
QTest.mouseClick(self.gui.genParamsTab.fileBtn, Qt.LeftButton)
QTest.mouseClick(self.gui.fileTab.backBtn, Qt.LeftButton)
QTest.mouseClick(self.gui.genParamsTab.backBtn, Qt.LeftButton)
QTest.qWait(1000)
self.assertEqual(len(self.gui.fileTab.visibleData), 100)
#self.assertEqual(self.gui.fileTab.tabStart.isVisible(), True)
#self.assertEqual(self.gui.fileTab.tabPrev.isVisible(), True)
#self.assertEqual(self.gui.fileTab.tabCurrent.isVisible(), False)
#self.assertEqual(self.gui.fileTab.tabNext.isVisible(), False)
#self.assertEqual(self.gui.fileTab.tabEnd.isVisible(), False)
# Test Tab Change
QTest.mouseClick(self.gui.fileTab.tabEnd, Qt.LeftButton, delay=1000)
self.assertEqual(len(self.gui.fileTab.visibleData), 1)
QTest.mouseClick(self.gui.fileTab.tabStart, Qt.LeftButton, delay=1000)
self.assertEqual(len(self.gui.fileTab.visibleData), 100)
# Test Filtering
self.gui.fileTab.filterName.setText("hundred")
QTest.mouseClick(self.gui.fileTab.startFilter, Qt.LeftButton)
# Test Selection
colPos = self.gui.fileTab.fileTableView.columnViewportPosition(self.initialCol)
rowPos = self.gui.fileTab.fileTableView.rowViewportPosition(self.initialRow)
QTest.mouseClick(self.gui.fileTab.fileTableView.viewport(), Qt.LeftButton, pos=QPoint(colPos + 1, rowPos + 1), delay=1000)
# Test Details
QTest.mouseClick(self.gui.fileTab.detailBtn, Qt.LeftButton)
items = self.gui.fileTab.detailsTable.findItems("fileName", Qt.MatchExactly)[0]
colPos = self.gui.fileTab.detailsTable.columnViewportPosition(items.column())
rowPos = self.gui.fileTab.detailsTable.rowViewportPosition(items.row())
QTest.mouseClick(self.gui.fileTab.detailsTable.viewport(), Qt.LeftButton, pos=QPoint(colPos + 1, rowPos + 1), delay=1000)
temp = self.gui.fileTab.detailsTable.item(self.gui.daqsTab.daqTable.currentRow(), 0)
self.gui.fileTab.detailsTable.item(self.gui.daqsTab.daqTable.currentRow(), 0).data(Qt.UserRole).toString()
## Uploads take time to abort so this is done split from the 'stop test' so as to allow time
model = self.gui.uploadsTab.tableWidget.findChild(QAbstractTableModel)
waitAbort = True
while waitAbort:
for row in range(model.rowCount()):
if model.data(model.index(row, 0)).toString() == "DmStationUITestSuite":
if model.data(model.index(row, 1)).toString() == "aborting":
if model.data(model.index(row, 1)).toString() == "aborted":
waitAbort = False
else:
QTest.mouseClick(self.gui.uploadsTab.refreshBtn, Qt.LeftButton)
QTest.qWait(1000)
else:
waitAbort = False
for row in range(model.rowCount()):
if model.data(model.index(row, 0)).toString() == "DmStationUITestSuite":
colPos = self.gui.uploadsTab.tableWidget.columnViewportPosition(self.initialCol)
......
......@@ -180,6 +180,11 @@ class ExperimentsTab(QWidget):
def updateView(self, index):
self.tableWidget.update(index)
# Returns the tables on this tab
def getTables(self):
tables = [self.tableWidget]
return tables
# Signals the parent to handle the right click event
def contextMenuEvent(self, event):
self.parent.handleRightClickMenu(self.tableWidget, event)
......
......@@ -401,16 +401,16 @@ class FileTab(QWidget):
def setVisibleData(self):
QApplication.setOverrideCursor(Qt.WaitCursor)
if self.doneBackground == 1:
visibleData = self.dataTable[(self.selectedBin - 1) * self.initialRowCount:((self.selectedBin - 1) * self.initialRowCount) + self.initialRowCount]
self.visibleData = self.dataTable[(self.selectedBin - 1) * self.initialRowCount:((self.selectedBin - 1) * self.initialRowCount) + self.initialRowCount]
else:
singleTab = self.fileCatApi.getExperimentFiles(self.parent.generalSettings['name'], {},
(self.selectedBin - 1) * self.initialRowCount, self.initialRowCount)
self.setupTab(singleTab)
visibleData = self.tempTable
#visibleData = self.dataTable
self.fileTableModel.update(visibleData)
self.visibleData = self.tempTable
#self.visibleData = self.dataTable
self.fileTableModel.update(self.visibleData)
self.fileTableModel.removeRows(0, self.initialRowCount)
self.fileTableModel.insertRows(0, min(self.initialRowCount, len(visibleData)))
self.fileTableModel.insertRows(0, min(self.initialRowCount, len(self.visibleData)))
self.fileTableView.clearFocus()
self.fileTableView.clearSelection()
QApplication.restoreOverrideCursor()
......@@ -499,6 +499,7 @@ class FileTab(QWidget):
self.detailsTable.setSortingEnabled(False)
row = self.fileTableView.selectionModel().selectedIndexes()[0]
index = ((self.selectedBin - 1) * self.initialRowCount) + self.fileTableView.model().mapToSource(row).row()
self.fileTableView.clearFocus()
if self.doneBackground == 1:
allInfo = self.fileCatApi.getExperimentFileById(self.parent.generalSettings['name'], self.dataTable[index][4])
else:
......@@ -529,13 +530,17 @@ class FileTab(QWidget):
# Toggles to show/hide the details table of the thing that is selected
def toggleDetails(self):
if self.showingDetails == 1:
self.detailsTable.clearFocus()
self.detailsTable.clearSelection()
self.fileTableView.show()
self.detailsTable.hide()
self.detailBtn.show()
self.showingDetails = 0
else:
self.showingDetails = 1
self.fileTableView.setSelectionMode(QAbstractItemView.NoSelection)
self.fileTableView.hide()
self.fileTableView.setSelectionMode(QAbstractItemView.SingleSelection)
self.fileDetails()
self.detailsTable.show()
self.detailBtn.hide()
......@@ -557,6 +562,11 @@ class FileTab(QWidget):
self.mainDataStorage = []
self.doneBackground = 0
# Returns the tables on this tab
def getTables(self):
tables = [self.fileTableView, self.detailsTable]
return tables
# Manually updates the tableView
def updateView(self, index):
if self.fileTableView.isVisible():
......
......@@ -512,6 +512,80 @@ class GenParamsTab(QWidget):
self.fileBtn.show()
self.parent.tabs.setCurrentIndex(2)
def fileStatDialog(self):
if 'name' in self.parent.generalSettings:
self.statsDialog = databaseStats.DatabaseStats(self, self.parent.generalSettings['name'])
self.statsDialog.exec_()
else:
self.saveExperimentDialog()
# Check that the experiment is valid before adding users
def checkExp(self):
#if self.parent.generalSettings.get('name') is None:
# self.modifyUserDialog()
# return
self.parent.setTab(1)
# Displays the file system for the user to navigate
def getFileSystem(self):
dialog = QFileDialog()
dialog.setFileMode(QFileDialog.DirectoryOnly)
dialog.setOption(QFileDialog.ShowDirsOnly, True)
if dialog.exec_():
directory = dialog.selectedFiles()[0]
self.dataField.setText(directory)
# Calendar overlay to select date
def toggleDate(self, fieldType=None):
if self.fieldType:
setattr(self.calendarPicker.a, 'fieldType', fieldType)
if self.calendarPicker.isVisible():
self.calendarPicker.setVisible(False)
else:
self.calendarPicker.setVisible(True)
# Returns the tables on this tab
def getTables(self):
tables = [self.currentUserTable]
return tables
# Popup to show the user common parameters
def toggleParams(self, specifier):
if specifier == 'daq':
self.daqDialog = daqParams.DaqParams(self, self.params)
self.daqDialog.exec_()
self.params = self.daqDialog.getParams()
self.sendText = self.daqDialog.getTextDict()
else:
self.uploadDialog = uploadParams.UploadParams(self, self.params)
self.uploadDialog.exec_()
self.params = self.uploadDialog.getParams()
self.sendText = self.uploadDialog.getTextDict()
# Updates the date
def setDate(self):
if self.sender().a.fieldType is 'Start':
self.startDateField.setDate(self.calendarPicker.selectedDate())
else:
self.endDateField.setDate(self.calendarPicker.selectedDate())
if self.endDateField.date() < self.startDateField.date() and self.sender().a.fieldType is 'End':
self.endDateField.setDate(self.startDateField.date())
self.dateInvalidDialog()
elif self.endDateField.date() < self.startDateField.date():
self.endDateField.setDate(self.startDateField.date())
self.toggleDate()
else:
self.toggleDate()
# Manually updates the tableView
def updateView(self, index):
self.currentUserTable.update(index)
# Signals the parent to handle the right click event
def contextMenuEvent(self, event):
self.parent.handleRightClickMenu(self.currentUserTable, event)
# Error dialog to prevent the adding of users to a nonexistant experiment
def modifyUserDialog(self):
msg = QMessageBox()
......@@ -585,72 +659,3 @@ class GenParamsTab(QWidget):
msg.setStandardButtons(QMessageBox.Ok)
msg.exec_()
def fileStatDialog(self):
if 'name' in self.parent.generalSettings:
self.statsDialog = databaseStats.DatabaseStats(self, self.parent.generalSettings['name'])
self.statsDialog.exec_()
else:
self.saveExperimentDialog()
# Check that the experiment is valid before adding users
def checkExp(self):
#if self.parent.generalSettings.get('name') is None:
# self.modifyUserDialog()
# return
self.parent.setTab(1)
# Displays the file system for the user to navigate
def getFileSystem(self):
dialog = QFileDialog()
dialog.setFileMode(QFileDialog.DirectoryOnly)
dialog.setOption(QFileDialog.ShowDirsOnly, True)
if dialog.exec_():
directory = dialog.selectedFiles()[0]
self.dataField.setText(directory)
# Calendar overlay to select date
def toggleDate(self, fieldType=None):
if fieldType:
setattr(self.calendarPicker.a, 'fieldType', fieldType)
if self.calendarPicker.isVisible():
self.calendarPicker.setVisible(False)
else:
self.calendarPicker.setVisible(True)
# Popup to show the user common parameters
def toggleParams(self, specifier):
if specifier == 'daq':
self.daqDialog = daqParams.DaqParams(self, self.params)
self.daqDialog.exec_()
self.params = self.daqDialog.getParams()
self.sendText = self.daqDialog.getTextDict()
else:
self.uploadDialog = uploadParams.UploadParams(self, self.params)
self.uploadDialog.exec_()
self.params = self.uploadDialog.getParams()
self.sendText = self.uploadDialog.getTextDict()
# Updates the date