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