08d4ff93733ed9703065a630b91374bb84b48eb0
[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 sface.config import config
9 from sface.sfiprocess import SfiProcess
10 from sface.screens.sfascreen import SfaScreen
11 from sface.sfidata import SfiData
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         for x in user_status.keys():
71             if (user_status[x] == status_data) and (x in color_status):
72                 painter.fillPath(path, color_status[x])
73
74         painter.setPen(QColor.fromRgb(0, 0, 0))
75         painter.drawText(textRect, Qt.AlignVCenter, QString(data))
76
77         painter.restore()
78
79 class UserView(QTableView):
80     def __init__(self, parent=None):
81         QTableView.__init__(self, parent)
82
83         self.setSelectionBehavior(QAbstractItemView.SelectRows)
84         self.setSelectionMode(QAbstractItemView.SingleSelection)
85         self.setAlternatingRowColors(True)
86         self.setAttribute(Qt.WA_MacShowFocusRect, 0)
87         self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
88         self.setToolTip("Double click on a row to change its status.")
89
90         self.setItemDelegateForColumn(0, UserNameDelegate(self))
91
92     def keyPressEvent(self, event):
93         if (event.key() == Qt.Key_Space):
94             self.toggleSelection()
95         else:
96             QTableView.keyPressEvent(self, event)
97
98     def mouseDoubleClickEvent(self, event):
99         self.toggleSelection()
100
101     def toggleSelection(self):
102         index = self.currentIndex()
103         model = index.model()
104         status_index = model.index(index.row(), MEMBERSHIP_STATUS_COLUMN, index.parent())
105         status_data = status_index.data().toString()
106         server_status_data = model.index(index.row(), SERVER_MEMBERSHIP_STATUS_COLUMN, index.parent()).data().toString()
107         node_index = model.index(index.row(), NAME_COLUMN, index.parent())
108         node_data = node_index.data().toString()
109
110         # This is a hostname
111         if status_data == user_status['in']:
112             model.setData(status_index, QString(user_status['remove']))
113         elif status_data == user_status['out']:
114             model.setData(status_index, QString(user_status['add']))
115         elif status_data in (user_status['add'], user_status['remove']):
116             if server_status_data == user_status["in"]:
117                 model.setData(status_index, QString(user_status['in']))
118             else:
119                 model.setData(status_index, QString(user_status['out']))
120
121         model.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), node_index, node_index)
122
123     def currentChanged(self, current, previous):
124         model = current.model()
125         node_index = model.index(current.row(), 0, current.parent())
126         node_data = node_index.data().toString()
127
128     def hideUnusableColumns(self):
129         self.hideColumn(SERVER_MEMBERSHIP_STATUS_COLUMN)
130
131 class UserModel(QStandardItemModel):
132     def __init__(self, rows=0, columns=4, parent=None):
133          QStandardItemModel.__init__(self, rows, columns, parent)
134
135     def updateModel(self, sliceRec):
136         self.clear()
137
138         added_persons = []
139         slice_persons = []
140
141         if sliceRec:
142             #for pi in sliceRec.get_field("PI", default=[]):
143             #    name = str(pi)
144             #    if not name in added_persons:
145             #         slice_persons.append({"name": name, "role": "PI", "member": user_status["in"]})
146             #         added_persons.append(name)
147
148             for researcher in sliceRec.get_field("researcher", default=[]):
149                 name = str(researcher)
150                 if not name in added_persons:
151                      slice_persons.append({"name": name, "role": "researcher", "member": user_status["in"]})
152                      added_persons.append(name)
153
154         userNames = SfiData().getAuthorityHrns(type="user")
155         for name in userNames:
156             if not name in added_persons:
157                 slice_persons.append({"name": name, "role": "", "member": user_status["out"]})
158                 added_persons.append(name)
159
160         rootItem = self.invisibleRootItem()
161
162         for person in slice_persons:
163             rootItem.appendRow([self.readOnlyItem(person["name"]),
164                                 self.readOnlyItem(person["member"]),
165                                 self.readOnlyItem(person["member"])])
166
167         headers = QStringList() << "User Name" << "Status" << "ServerStatus"
168         self.setHorizontalHeaderLabels(headers)
169
170     def readOnlyItem(self, x):
171         item = QStandardItem(QString(x))
172         item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
173         return item
174
175     def updateRecord(self, slicerec):
176         change = False
177
178         item = self.invisibleRootItem()
179         children = item.rowCount()
180         for row in range(0, children):
181             childName = str(item.child(row, NAME_COLUMN).data(Qt.DisplayRole).toString())
182             childStatus = str(item.child(row, MEMBERSHIP_STATUS_COLUMN).data(Qt.DisplayRole).toString())
183
184             if (childStatus == user_status['add']):
185                 researcher = slicerec.get_field("researcher", [])
186                 researcher.append(childName)
187                 slicerec["researcher"] = researcher
188                 change = True
189             elif (childStatus == user_status['remove']):
190                 if childName in slicerec.get_field("PI"):
191                      slicerec.get_field("PI").remove(childName)
192                 if childName in slicerec.get_field("researcher"):
193                      slicerec.get_field("researcher").remove(childName)
194                 change = True
195
196         return change
197
198     def getResearchers(self):
199         researchers = []
200         item = self.invisibleRootItem()
201         children = item.rowCount()
202         for row in range(0, children):
203             childName = str(item.child(row, NAME_COLUMN).data(Qt.DisplayRole).toString())
204             childStatus = str(item.child(row, MEMBERSHIP_STATUS_COLUMN).data(Qt.DisplayRole).toString())
205
206             if (childStatus == user_status['add']) or (childStatus == user_status['in']):
207                 researchers.append(childName)
208
209         return researchers
210
211 class UsersWidget(QWidget):
212     def __init__(self, parent):
213         QWidget.__init__(self, parent)
214
215         self.process = SfiProcess(self)
216
217         self.slicename = QLabel("", self)
218         self.updateSliceName()
219         self.slicename.setScaledContents(False)
220         searchlabel = QLabel ("Search: ", self)
221         searchlabel.setScaledContents(False)
222         searchbox = QLineEdit(self)
223         searchbox.setAttribute(Qt.WA_MacShowFocusRect, 0)
224
225         toplayout = QHBoxLayout()
226         toplayout.addWidget(self.slicename, 0, Qt.AlignLeft)
227         toplayout.addStretch()
228         toplayout.addWidget(searchlabel, 0, Qt.AlignRight)
229         toplayout.addWidget(searchbox, 0, Qt.AlignRight)
230
231         self.userView = UserView()
232
233         refresh = QPushButton("Refresh", self)
234         refresh.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
235         submit = QPushButton("Submit", self)
236         submit.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
237
238         bottomlayout = QHBoxLayout()
239         bottomlayout.addWidget(refresh, 0, Qt.AlignLeft)
240         bottomlayout.addStretch()
241         bottomlayout.addWidget(submit, 0, Qt.AlignRight)
242
243         layout = QVBoxLayout()
244         layout.addLayout(toplayout)
245         layout.addWidget(self.userView)
246         layout.addLayout(bottomlayout)
247         self.setLayout(layout)
248         self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
249
250         self.userModel = UserModel(parent=self)
251
252         self.connect(refresh, SIGNAL('clicked()'), self.refresh)
253         self.connect(submit, SIGNAL('clicked()'), self.submit)
254
255         self.updateView()
256
257     def submitFinished(self):
258         self.disconnect(self.process, SIGNAL('finished()'), self.submitFinished)
259
260         faultString = self.process.getFaultString()
261         if not faultString:
262             self.setStatus("<font color='green'>Slice user data submitted.</font>")
263             QTimer.singleShot(1000, self.refresh)
264         else:
265             self.setStatus("<font color='red'>Slice user submit failed: %s</font>" % (faultString))
266
267     def getSliceRecordFinished(self):
268         self.disconnect(self.process, SIGNAL('finished()'), self.getSliceRecordFinished)
269
270         faultString = self.process.getFaultString()
271         if not faultString:
272             self.setStatus("<font color='green'>Slice record refreshed.</font>")
273             self.refreshAuthority()
274         else:
275             self.setStatus("<font color='red'>Slice rec refresh error: %s</font>" % (faultString))
276
277     def getAuthorityRecordFinished(self):
278         self.disconnect(self.process, SIGNAL('finished()'), self.getAuthorityRecordFinished)
279
280         faultString = self.process.getFaultString()
281         if not faultString:
282             self.setStatus("<font color='green'>User data refreshed.</font>")
283             self.updateView()
284             #self.parent().signalAll("usersUpdated")
285         else:
286             self.setStatus("<font color='red'>Authority rec refresh error: %s</font>" % (faultString))
287
288     def setStatus(self, msg, timeout=None):
289         self.parent().setStatus(msg, timeout)
290
291     def checkRunningProcess(self):
292         if self.process.isRunning():
293             self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
294             return True
295         return False
296
297     def submit(self):
298         if self.checkRunningProcess():
299             return
300
301         rec = SfiData().getSliceRecord()
302         change = self.userModel.updateRecord(rec)
303
304         if not change:
305             self.setStatus("<font color=red>No change in slice data. Not submitting!</font>", timeout=3000)
306             return
307
308         rec_file = config.getSliceRecordFile()
309         file(rec_file, "w").write(rec.save_to_string())
310
311         self.connect(self.process, SIGNAL('finished()'), self.submitFinished)
312
313         self.process.updateRecord(rec_file)
314         self.setStatus("Sending slice record. This will take some time...")
315
316     def refresh(self):
317         if not config.getSlice():
318             self.setStatus("<font color='red'>Slice not set yet!</font>")
319             return
320
321         if self.process.isRunning():
322             self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
323             return
324
325         self.connect(self.process, SIGNAL('finished()'), self.getSliceRecordFinished)
326
327         self.process.getSliceRecord()
328         self.setStatus("Refreshing slice record. This will take some time...")
329
330     def refreshAuthority(self):
331         self.connect(self.process, SIGNAL('finished()'), self.getAuthorityRecordFinished)
332
333         self.process.listRecords(config.getAuthority(), None)
334         self.setStatus("Refreshing user records. This will take some time...")
335
336     def updateView(self):
337         sliceRec = SfiData().getSliceRecord()
338
339         if not sliceRec:
340             # wait until we've resolved the slicerecord before displaying
341             # anything to the user.
342             self.userModel.clear()
343         else:
344             self.userModel.updateModel(sliceRec)
345
346         self.userView.setModel(self.userModel)
347         self.userView.hideColumn(SERVER_MEMBERSHIP_STATUS_COLUMN)
348         self.userView.resizeColumnToContents(0)
349
350     def updateSliceName(self):
351         self.slicename.setText("Slice : %s" % (config.getSlice() or "None"))
352
353
354
355
356 class UserScreen(SfaScreen):
357     def __init__(self, parent):
358         SfaScreen.__init__(self, parent)
359
360         slice = UsersWidget(self)
361         self.init(slice, "Users", "OneLab SFA crawler")
362
363     def configurationChanged(self):
364         self.widget.updateSliceName()
365         self.widget.updateView()
366
367