Commit 6c090ec2 authored by Mark Hymers's avatar Mark Hymers Committed by Joe Lyons
Browse files

Add first pass at GUI


Signed-off-by: Mark Hymers's avatarMark Hymers <mark.hymers@hankel.co.uk>
parent 16b2bc49
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1258</width>
<height>781</height>
</rect>
</property>
<property name="windowTitle">
<string>YIAS Transfer GUI</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTableView" name="scanView">
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
</widget>
</item>
<item>
<widget class="QWidget" name="widget" native="true">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="deleteButton">
<property name="text">
<string>&amp;Delete</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="editButton">
<property name="text">
<string>&amp;Edit</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="uploadButton">
<property name="text">
<string>&amp;Upload</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="refreshButton">
<property name="text">
<string>&amp;Refresh</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="exitButton">
<property name="text">
<string>E&amp;xit</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1258</width>
<height>20</height>
</rect>
</property>
</widget>
<widget class="QStatusBar" name="statusbar"/>
</widget>
<resources/>
<connections/>
</ui>
#!/usr/bin/python3
import sys
import operator
import tempfile
from os import unlink, makedirs, symlink
from os.path import join, basename, dirname, exists, abspath
from glob import glob
from datetime import datetime
from pytz import utc
import dateutil.parser
import h5py
import yias
from PyQt5 import uic
from PyQt5.QtWidgets import (QMainWindow, QApplication, QWidget, QAction,
QTableWidget, QTableWidgetItem, QVBoxLayout,
QAbstractItemView, QHeaderView, QMessageBox)
from PyQt5.QtGui import QIcon, QColor
from PyQt5.QtCore import pyqtSlot, QAbstractTableModel, Qt, QVariant
YIAS_PATH = dirname(yias.__file__)
Ui_MainWindow, QtBaseClass = uic.loadUiType(join(YIAS_PATH, 'gui', 'megtransfergui.ui'))
def find_scans(base_path):
raw_scans = set([basename(x) for x in glob(join(base_path, 'raw', '*.hdf5'))])
processed_scans = set([basename(x) for x in glob(join(base_path, 'processed', '*.hdf5'))])
# Find scans in just raw, both and just processed
both_scans = sorted(list(raw_scans.intersection(processed_scans)))
raw_only = sorted(list(raw_scans.difference(processed_scans)))
proc_only = sorted(list(processed_scans.difference(raw_scans)))
return both_scans, raw_only, proc_only
def parse_scan_file(filename, raw, proc, base_path):
if proc:
filepath = join(base_path, 'processed', filename)
else:
filepath = join(base_path, 'raw', filename)
data = {}
data['filename'] = filename
data['raw'] = raw
data['proc'] = proc
data['subjid'] = 'UNKNOWN'
data['anonymous'] = False
data['numacqs'] = 0
data['protocol'] = 'UNKNOWN'
data['scantime'] = datetime(1901, 1, 1, tzinfo=utc)
with h5py.File(filepath, 'r') as f:
try:
data['subjid'] = f['subject'].attrs['id']
data['anonymous'] = f['subject'].attrs['anonymous'] == 1
data['numacqs'] = len([x for x in f['acquisitions'].keys() if x != 'default'])
try:
# Could be missing if we're reading a raw file
data['protocol'] = f['config']['protocol'].attrs['protocol_name']
except Exception as e:
pass
try:
# Could be missing if we're reading a raw file
data['scantime'] = dateutil.parser.parse(f['acquisitions']['0'].attrs['start_time'])
except Exception as e:
pass
except Exception as e:
print(filepath, e)
return data
def get_scan_data(base_path):
both_scans, raw_only, proc_only = find_scans(base_path)
# all_scans is path, raw, proc, subj, anon
all_scans = []
for scan in both_scans:
all_scans.append(parse_scan_file(scan, True, True, base_path))
for scan in raw_only:
all_scans.append(parse_scan_file(scan, True, False, base_path))
for scan in proc_only:
all_scans.append(parse_scan_file(scan, False, True, base_path))
return all_scans
COL_SCANTIME = 0
COL_PROTOCOL = 1
COL_SUBJID = 2
COL_ANONYMISED = 3
COL_NUMACQS = 4
COL_FILENAME = 5
COL_KEYS = ['scantime', 'protocol', 'subjid', 'anonymous', 'numacqs', 'filename']
class ScanModel(QAbstractTableModel):
def __init__(self, *args, scans=None, **kwargs):
super(ScanModel, self).__init__(*args, **kwargs)
self.scans = scans or []
self.headers = ['Date / Time', 'Protocol', 'Subject ID', 'Anonymised', 'Num Acqs', 'Filename']
def UpdateData(self, scans):
self.beginResetModel()
self.scans = scans
self.endResetModel()
def headerData(self, col, orientation, role):
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
return self.headers[col]
def sort(self, col, order):
self.layoutAboutToBeChanged.emit()
self.scans = sorted(self.scans, key=operator.itemgetter(COL_KEYS[col]))
if order == Qt.DescendingOrder:
self.scans.reverse()
self.layoutChanged.emit()
def data(self, index, role):
if role == Qt.DisplayRole:
data = self.scans[index.row()]
if index.column() in [COL_FILENAME, COL_SUBJID, COL_NUMACQS, COL_PROTOCOL]:
return data[COL_KEYS[index.column()]]
elif index.column() == COL_SCANTIME:
if data['scantime'] == datetime(1901, 1, 1):
return 'Unknown'
else:
return data['scantime'].strftime('%Y%m%d %H:%M')
else:
return QVariant()
if role == Qt.DecorationRole:
data = self.scans[index.row()]
# Always put this at the start no matter which column is there
if index.column() == 0:
if data['raw'] and data['proc']:
return QColor('green')
elif data['raw']:
return QColor('yellow')
else:
# Processed only - strange
return QColor('orange')
if index.column() == COL_ANONYMISED:
if data['anonymous']:
return QColor('green')
else:
return QColor('red')
return QVariant()
if role == Qt.ItemDataRole:
return self.scans[index.row()]['filename']
def rowCount(self, index):
return len(self.scans)
def columnCount(self, index):
return len(self.headers)
class TransferApp(QMainWindow, Ui_MainWindow):
def __init__(self, base_path, upload_path):
QMainWindow.__init__(self)
Ui_MainWindow.__init__(self)
self.setupUi(self)
self.base_path = base_path
self.upload_path = upload_path
self.model = ScanModel(scans=get_scan_data(self.base_path))
self.scanView.setModel(self.model)
self.scanView.setSelectionBehavior(QAbstractItemView.SelectRows)
self.scanView.setSortingEnabled(True)
self.scanView.selectionModel().selectionChanged.connect(self.UpdateSelection)
self.deleteButton.clicked.connect(self.OnDelete)
self.editButton.clicked.connect(self.OnEdit)
self.uploadButton.clicked.connect(self.OnUpload)
self.refreshButton.clicked.connect(self.OnRefresh)
self.exitButton.clicked.connect(self.close)
header = self.scanView.horizontalHeader()
header.setSectionResizeMode(QHeaderView.ResizeToContents)
header.setSectionResizeMode(self.model.columnCount(0) - 1, QHeaderView.Stretch)
self.UpdateSelection(None)
def UpdateSelection(self, evt):
# Only show buttons if we have a selection
val = len(self.scanView.selectionModel().selection()) >= 1
self.deleteButton.setEnabled(val)
self.editButton.setEnabled(val)
self.uploadButton.setEnabled(val)
def OnRefresh(self, evt):
self.model.UpdateData(get_scan_data(self.base_path))
self.UpdateSelection(None)
def OnDelete(self, evt):
if len(self.scanView.selectionModel().selection()) < 1:
return
# Find our filenames
filenames = set()
for idx in self.scanView.selectionModel().selectedIndexes():
filenames.add(self.model.data(idx, Qt.ItemDataRole))
raw_files = []
proc_files = []
for filename in filenames:
raw_name = abspath(join(base_path, 'raw', filename))
if exists(raw_name):
raw_files.append(raw_name)
proc_name = abspath(join(base_path, 'processed', filename))
if exists(proc_name):
proc_files.append(proc_name)
ans = QMessageBox.question(self, "Delete Files",
"You will delete {} raw and {} processed files. Continue?".format(len(raw_files), len(proc_files)))
if ans == QMessageBox.Yes:
removed = []
try:
for filename in raw_files:
unlink(filename)
removed.append(filename)
for filename in proc_files:
unlink(filename)
removed.append(filename)
except Exception as e:
QMessageBox.warning(self, "Could not delete all files ({}). Deleted {} files".format(str(e), len(filename)))
self.OnRefresh(None)
def OnEdit(self, evt):
if len(self.scanView.selectionModel().selection()) != 1:
return
# Find our filenames
filenames = set()
for idx in self.scanView.selectionModel().selectedIndexes():
filenames.add(self.model.data(idx, Qt.ItemDataRole))
if len(filenames) != 1:
QMessageBox.warning(self, "Edit", "To edit select only one row")
return
# TODO: Allow editing of patient ID and study ID
QMessageBox.warning(self, "Edit", "Editing not yet implemented")
def OnUpload(self, evt):
if len(self.scanView.selectionModel().selection()) < 1:
return
# Find our filenames
filenames = set()
for idx in self.scanView.selectionModel().selectedIndexes():
filenames.add(self.model.data(idx, Qt.ItemDataRole))
# Create one incoming directory for each file
for filename in filenames:
try:
# Create a temporary directory
prefix = 'incoming_{}'.format(datetime.now().strftime('%Y%M%d%H%M%S'))
inc_dir = tempfile.mkdtemp(dir=self.upload_path, prefix=prefix)
raw_name = abspath(join(base_path, 'raw', filename))
if exists(raw_name):
symlink(raw_name, join(inc_dir, 'raw.hdf5'))
proc_name = abspath(join(base_path, 'processed', filename))
if exists(proc_name):
symlink(raw_name, join(inc_dir, 'processed.hdf5'))
# Finally, touch the complete file
with open(inc_dir + '.complete', 'w') as f:
pass
except Exception as e:
QMessageBox.warning(self, "Could not upload all files ({})".format(str(e)))
return
QMessageBox.information(self, "Upload", "Uploaded {} filesets".format(len(filenames)))
if __name__ == '__main__':
app = QApplication(sys.argv)
if len(sys.argv) > 1:
base_path = sys.argv[1]
else:
base_path = '/srv/data'
if len(sys.argv) > 2:
upload_path = sys.argv[2]
else:
upload_path = '/srv/incoming'
win = TransferApp(base_path, upload_path)
win.show()
sys.exit(app.exec_())
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment