better error reporting in userscreen
[sface.git] / sface / screens / userscreen.py
1
2 import datetime
3 import os
4 import pickle
5 from PyQt4.QtCore import *
6 from PyQt4.QtGui import *
7
8 from sfa.util.record import SfaRecord, SliceRecord, AuthorityRecord, UserRecord
9 from sface.config import config
10 from sface.sfiprocess import SfiProcess
11 from sface.screens.sfascreen import SfaScreen
12
13 NAME_COLUMN = 0
14 #ROLE_COLUMN = 1
15 MEMBERSHIP_STATUS_COLUMN = 1
16 SERVER_MEMBERSHIP_STATUS_COLUMN = 2
17
18 # maximum length of a name to display before clipping
19 NAME_MAX_LEN = 48
20
21 user_status = { "in": "Already Selected",
22                 "out": "Not Selected",
23                 "add": "To be Added",
24                 "remove": "To be Removed"}
25
26 color_status = { "in": QColor.fromRgb(0, 250, 250),
27                  "add": QColor.fromRgb(0, 250, 0),
28                  "remove": QColor.fromRgb(250, 0, 0) }
29
30
31 class UserNameDelegate(QStyledItemDelegate):
32     def __init__(self, parent):
33         QStyledItemDelegate.__init__(self, parent)
34
35     def displayText(self, value, locale):
36         data = str(QStyledItemDelegate.displayText(self, value, locale))
37         if (len(data)>NAME_MAX_LEN):
38             data = data[:(NAME_MAX_LEN-3)] + "..."
39         return QString(data)
40
41     def paint(self, painter, option, index):
42         model = index.model()
43         data = str(self.displayText(index.data(), QLocale()))
44         status_index = model.index(index.row(), MEMBERSHIP_STATUS_COLUMN, index.parent())
45         status_data = status_index.data().toString()
46
47         fm = QFontMetrics(option.font)
48         rect = QRect(option.rect)
49
50         rect.setHeight(rect.height() - 2)
51         rect.setWidth(fm.width(QString(data)) + 6)
52         rect.setX(rect.x() + 5)
53         rect.setY(rect.y() - 1)
54
55         textRect = QRect(option.rect)
56         textRect.setWidth(fm.width(QString(data)) + 6)
57         textRect.setX(rect.x())
58
59         x, y, h, w = rect.x(), rect.y(), rect.height(), rect.width()
60
61         path = QPainterPath()
62         path.addRoundedRect(x - 1, y + 1, w, h, 4, 4)
63
64         painter.save()
65         painter.setRenderHint(QPainter.Antialiasing)
66
67         if option.state & QStyle.State_Selected:
68             painter.fillRect(option.rect, option.palette.color(QPalette.Active, QPalette.Highlight))
69
70         color = None
71         for x in user_status.keys():
72             if (user_status[x] == status_data) and (x in color_status):
73                 color = color_status[x]
74
75         if color != None:
76             painter.fillPath(path, color)
77         painter.setPen(QColor.fromRgb(0, 0, 0))
78         painter.drawText(textRect, Qt.AlignVCenter, QString(data))
79
80         painter.restore()
81
82 class UserView(QTableView):
83     def __init__(self, parent=None):
84         QTableView.__init__(self, parent)
85
86         self.setSelectionBehavior(QAbstractItemView.SelectRows)
87         self.setSelectionMode(QAbstractItemView.SingleSelection)
88         self.setAlternatingRowColors(True)
89         self.setAttribute(Qt.WA_MacShowFocusRect, 0)
90         self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
91         self.setToolTip("Double click on a row to change its status.")
92
93         self.setItemDelegateForColumn(0, UserNameDelegate(self))
94
95     def keyPressEvent(self, event):
96         if (event.key() == Qt.Key_Space):
97             self.toggleSelection()
98         else:
99             QTableView.keyPressEvent(self, event)
100
101     def mouseDoubleClickEvent(self, event):
102         self.toggleSelection()
103
104     def toggleSelection(self):
105         index = self.currentIndex()
106         model = index.model()
107         status_index = model.index(index.row(), MEMBERSHIP_STATUS_COLUMN, index.parent())
108         status_data = status_index.data().toString()
109         server_status_data = model.index(index.row(), SERVER_MEMBERSHIP_STATUS_COLUMN, index.parent()).data().toString()
110         node_index = model.index(index.row(), NAME_COLUMN, index.parent())
111         node_data = node_index.data().toString()
112
113         # This is a hostname
114         if status_data == user_status['in']:
115             model.setData(status_index, QString(user_status['remove']))
116         elif status_data == user_status['out']:
117             model.setData(status_index, QString(user_status['add']))
118         elif status_data in (user_status['add'], user_status['remove']):
119             if server_status_data == user_status["in"]:
120                 model.setData(status_index, QString(user_status['in']))
121             else:
122                 model.setData(status_index, QString(user_status['out']))
123
124         model.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), node_index, node_index)
125
126     def currentChanged(self, current, previous):
127         model = current.model()
128         node_index = model.index(current.row(), 0, current.parent())
129         node_data = node_index.data().toString()
130         self.emit(SIGNAL('hostnameClicked(QString)'), node_data)
131
132     def hideUnusableColumns(self):
133         self.hideColumn(SERVER_MEMBERSHIP_STATUS_COLUMN)
134
135 class UserModel(QStandardItemModel):
136     def __init__(self, rows=0, columns=4, parent=None):
137          QStandardItemModel.__init__(self, rows, columns, parent)
138
139     def updateModel(self, sliceRec):
140         self.clear()
141
142         added_persons = []
143         slice_persons = []
144
145         if sliceRec:
146             #for pi in sliceRec.get_field("PI", default=[]):
147             #    name = str(pi)
148             #    if not name in added_persons:
149             #         slice_persons.append({"name": name, "role": "PI", "member": user_status["in"]})
150             #         added_persons.append(name)
151
152             for researcher in sliceRec.get_field("researcher", default=[]):
153                 name = str(researcher)
154                 if not name in added_persons:
155                      slice_persons.append({"name": name, "role": "researcher", "member": user_status["in"]})
156                      added_persons.append(name)
157
158         i=0
159         while (os.path.exists(config.getAuthorityListFile(i))):
160             rec = self.readUserRecord(i)
161             if rec:
162                 name = str(rec.get_name())
163                 if not name in added_persons:
164                     slice_persons.append({"name": name, "role": "", "member": user_status["out"]})
165                     added_persons.append(name)
166             i=i+1
167
168         rootItem = self.invisibleRootItem()
169
170         for person in slice_persons:
171             rootItem.appendRow([self.readOnlyItem(person["name"]),
172                                 self.readOnlyItem(person["member"]),
173                                 self.readOnlyItem(person["member"])])
174
175         headers = QStringList() << "User Name" << "Status" << "ServerStatus"
176         self.setHorizontalHeaderLabels(headers)
177
178     def readOnlyItem(self, x):
179         item = QStandardItem(QString(x))
180         item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
181         return item
182
183     def updateRecord(self, slicerec):
184         change = False
185
186         item = self.invisibleRootItem()
187         children = item.rowCount()
188         for row in range(0, children):
189             childName = str(item.child(row, NAME_COLUMN).data(Qt.DisplayRole).toString())
190             childStatus = str(item.child(row, MEMBERSHIP_STATUS_COLUMN).data(Qt.DisplayRole).toString())
191
192             if (childStatus == user_status['add']):
193                 researcher = slicerec.get_field("researcher", [])
194                 researcher.append(childName)
195                 slicerec["researcher"] = researcher
196                 change = True
197             elif (childStatus == user_status['remove']):
198                 if childName in slicerec.get_field("PI"):
199                      slicerec.get_field("PI").remove(childName)
200                 if childName in slicerec.get_field("researcher"):
201                      slicerec.get_field("researcher").remove(childName)
202                 change = True
203
204         return change
205
206     def getResearchers(self):
207         researchers = []
208         item = self.invisibleRootItem()
209         children = item.rowCount()
210         for row in range(0, children):
211             childName = str(item.child(row, NAME_COLUMN).data(Qt.DisplayRole).toString())
212             childStatus = str(item.child(row, MEMBERSHIP_STATUS_COLUMN).data(Qt.DisplayRole).toString())
213
214             if (childStatus == user_status['add']) or (childStatus == user_status['in']):
215                 researchers.append(childName)
216
217         return researchers
218
219     def readUserRecord(self, i):
220         rec_file = config.getAuthorityListFile(i)
221         if os.path.exists(rec_file):
222             xml = open(rec_file).read()
223             rec = UserRecord()
224             rec.load_from_string(xml)
225             return rec
226         return None
227
228 class UsersWidget(QWidget):
229     def __init__(self, parent):
230         QWidget.__init__(self, parent)
231
232         self.process = SfiProcess(self)
233
234         self.slicename = QLabel("", self)
235         self.updateSliceName()
236         self.slicename.setScaledContents(False)
237         searchlabel = QLabel ("Search: ", self)
238         searchlabel.setScaledContents(False)
239         searchbox = QLineEdit(self)
240         searchbox.setAttribute(Qt.WA_MacShowFocusRect, 0)
241
242         toplayout = QHBoxLayout()
243         toplayout.addWidget(self.slicename, 0, Qt.AlignLeft)
244         toplayout.addStretch()
245         toplayout.addWidget(searchlabel, 0, Qt.AlignRight)
246         toplayout.addWidget(searchbox, 0, Qt.AlignRight)
247
248         self.userView = UserView()
249
250         refresh = QPushButton("Refresh", self)
251         refresh.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
252         submit = QPushButton("Submit", self)
253         submit.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
254
255         bottomlayout = QHBoxLayout()
256         bottomlayout.addWidget(refresh, 0, Qt.AlignLeft)
257         bottomlayout.addStretch()
258         bottomlayout.addWidget(submit, 0, Qt.AlignRight)
259
260         layout = QVBoxLayout()
261         layout.addLayout(toplayout)
262         layout.addWidget(self.userView)
263         layout.addLayout(bottomlayout)
264         self.setLayout(layout)
265         self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
266
267         self.userModel = UserModel(parent=self)
268
269         self.connect(refresh, SIGNAL('clicked()'), self.refresh)
270         self.connect(submit, SIGNAL('clicked()'), self.submit)
271
272         self.updateView()
273
274     def submitFinished(self):
275         self.disconnect(self.process, SIGNAL('finished()'), self.submitFinished)
276
277         faultString = self.process.getFaultString()
278         if not faultString:
279             self.setStatus("<font color='green'>Slice user data submitted.</font>")
280             QTimer.singleShot(1000, self.refresh)
281         else:
282             self.setStatus("<font color='red'>Slice user submit failed: %s</font>" % (faultString))
283
284     def getSliceRecordFinished(self):
285         self.disconnect(self.process, SIGNAL('finished()'), self.getSliceRecordFinished)
286
287         faultString = self.process.getFaultString()
288         if not faultString:
289             self.setStatus("<font color='green'>Slice record refreshed.</font>")
290             self.refreshAuthority()
291         else:
292             self.setStatus("<font color='red'>Slice rec refresh error: %s</font>" % (faultString))
293
294     def getAuthorityRecordFinished(self):
295         self.disconnect(self.process, SIGNAL('finished()'), self.getAuthorityRecordFinished)
296
297         faultString = self.process.getFaultString()
298         if not faultString:
299             self.setStatus("<font color='green'>User data refreshed.</font>")
300             self.updateView()
301             #self.parent().signalAll("usersUpdated")
302         else:
303             self.setStatus("<font color='red'>Authority rec refresh error: %s</font>" % (faultString))
304
305     def readSliceRecord(self):
306         rec_file = config.getSliceRecordFile()
307         if os.path.exists(rec_file):
308             xml = open(rec_file).read()
309             rec = SliceRecord()
310             rec.load_from_string(xml)
311             return rec
312         return None
313
314     def readAuthorityRecord(self):
315         rec_file = config.getAuthorityRecordFile()
316         if os.path.exists(rec_file):
317             xml = open(rec_file).read()
318             rec = AuthorityRecord()
319             rec.load_from_string(xml)
320             return rec
321         return None
322
323     def setStatus(self, msg, timeout=None):
324         self.parent().setStatus(msg, timeout)
325
326     def checkRunningProcess(self):
327         if self.process.isRunning():
328             self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
329             return True
330         return False
331
332     def submit(self):
333         if self.checkRunningProcess():
334             return
335
336         rec = self.readSliceRecord()
337         change = self.userModel.updateRecord(rec)
338
339         if not change:
340             self.setStatus("<font color=red>No change in slice data. Not submitting!</font>", timeout=3000)
341             return
342
343         rec_file = config.getSliceRecordFile()
344         file(rec_file, "w").write(rec.save_to_string())
345
346         self.connect(self.process, SIGNAL('finished()'), self.submitFinished)
347
348         self.process.updateRecord(rec_file)
349         self.setStatus("Sending slice record. This will take some time...")
350
351     def refresh(self):
352         if not config.getSlice():
353             self.setStatus("<font color='red'>Slice not set yet!</font>")
354             return
355
356         if self.process.isRunning():
357             self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
358             return
359
360         self.connect(self.process, SIGNAL('finished()'), self.getSliceRecordFinished)
361
362         self.process.getSliceRecord()
363         self.setStatus("Refreshing slice record. This will take some time...")
364
365     def refreshAuthority(self):
366         self.connect(self.process, SIGNAL('finished()'), self.getAuthorityRecordFinished)
367
368         self.process.listRecords(config.getAuthority(), "user", config.getAuthorityListFile())
369         self.setStatus("Refreshing user records. This will take some time...")
370
371     def updateView(self):
372         sliceRec = self.readSliceRecord()
373
374         if not sliceRec:
375             # wait until we've resolved the slicerecord before displaying
376             # anything to the user.
377             self.userModel.clear()
378         else:
379             self.userModel.updateModel(sliceRec)
380
381         self.userView.setModel(self.userModel)
382         self.userView.hideColumn(SERVER_MEMBERSHIP_STATUS_COLUMN)
383         self.userView.resizeColumnToContents(0)
384
385     def updateSliceName(self):
386         self.slicename.setText("Slice : %s" % (config.getSlice() or "None"))
387
388
389
390
391 class UserScreen(SfaScreen):
392     def __init__(self, parent):
393         SfaScreen.__init__(self, parent)
394
395         slice = UsersWidget(self)
396         self.init(slice, "Users", "OneLab SFA crawler")
397
398     def configurationChanged(self):
399         self.widget.updateSliceName()
400         self.widget.updateView()
401
402