color coding for user screen
[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.setStatus("<font color='green'>Slice data submitted.</font>")
276         QTimer.singleShot(1000, self.refresh)
277
278     def getSliceRecordFinished(self):
279         self.setStatus("<font color='green'>Authority data refreshed.</font>", timeout=5000)
280         self.refreshAuthority()
281
282     def getAuthorityRecordFinished(self):
283         self.setStatus("<font color='green'>Slice data refreshed.</font>", timeout=5000)
284         self.updateView()
285         #self.parent().signalAll("usersUpdated")
286
287     def readSliceRecord(self):
288         rec_file = config.getSliceRecordFile()
289         if os.path.exists(rec_file):
290             xml = open(rec_file).read()
291             rec = SliceRecord()
292             rec.load_from_string(xml)
293             return rec
294         return None
295
296     def readAuthorityRecord(self):
297         rec_file = config.getAuthorityRecordFile()
298         if os.path.exists(rec_file):
299             xml = open(rec_file).read()
300             rec = AuthorityRecord()
301             rec.load_from_string(xml)
302             return rec
303         return None
304
305     def setStatus(self, msg, timeout=None):
306         self.parent().setStatus(msg, timeout)
307
308     def checkRunningProcess(self):
309         if self.process.isRunning():
310             self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
311             return True
312         return False
313
314     def submit(self):
315         if self.checkRunningProcess():
316             return
317
318         rec = self.readSliceRecord()
319         change = self.userModel.updateRecord(rec)
320
321         if not change:
322             self.setStatus("<font color=red>No change in slice data. Not submitting!</font>", timeout=3000)
323             return
324
325         rec_file = config.getSliceRecordFile()
326         file(rec_file, "w").write(rec.save_to_string())
327
328         self.disconnect(self.process, SIGNAL('finished()'), self.getAuthorityRecordFinished)
329         self.connect(self.process, SIGNAL('finished()'), self.submitFinished)
330
331         self.process.updateRecord(rec_file)
332         self.setStatus("Sending slice record. This will take some time...")
333
334     def refresh(self):
335         if not config.getSlice():
336             self.setStatus("<font color='red'>Slice not set yet!</font>")
337             return
338
339         if self.process.isRunning():
340             self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
341             return
342
343         self.disconnect(self.process, SIGNAL('finished()'), self.submitFinished)
344         self.connect(self.process, SIGNAL('finished()'), self.getSliceRecordFinished)
345
346         self.process.getSliceRecord()
347         self.setStatus("Refreshing slice record. This will take some time...")
348
349     def refreshAuthority(self):
350         self.disconnect(self.process, SIGNAL('finished()'), self.getSliceRecordFinished)
351         self.connect(self.process, SIGNAL('finished()'), self.getAuthorityRecordFinished)
352
353         self.process.listRecords(config.getAuthority(), "user", config.getAuthorityListFile())
354         self.setStatus("Refreshing user records. This will take some time...")
355
356     def updateView(self):
357         sliceRec = self.readSliceRecord()
358
359         if not sliceRec:
360             # wait until we've resolved the slicerecord before displaying
361             # anything to the user.
362             self.userModel.clear()
363         else:
364             self.userModel.updateModel(sliceRec)
365
366         self.userView.setModel(self.userModel)
367         self.userView.hideColumn(SERVER_MEMBERSHIP_STATUS_COLUMN)
368         self.userView.resizeColumnToContents(0)
369
370     def updateSliceName(self):
371         self.slicename.setText("Slice : %s" % (config.getSlice() or "None"))
372
373
374
375
376 class UserScreen(SfaScreen):
377     def __init__(self, parent):
378         SfaScreen.__init__(self, parent)
379
380         slice = UsersWidget(self)
381         self.init(slice, "Users", "OneLab SFA crawler")
382
383     def configurationChanged(self):
384         self.widget.updateSliceName()
385         self.widget.updateView()
386
387