source: orange/Orange/OrangeWidgets/OWDatabasesUpdate.py @ 11437:c4f644fd1c80

Revision 11437:c4f644fd1c80, 16.5 KB checked in by Ales Erjavec <ales.erjavec@…>, 13 months ago (diff)

Added 'Last Updated' column to 'OWDatabasesUpdate'

Line 
1from __future__ import with_statement
2
3import os
4import sys
5
6from datetime import datetime
7
8import Orange
9
10from Orange.utils import serverfiles, environ
11from Orange.utils.serverfiles import sizeformat as sizeof_fmt
12
13from OWWidget import *
14from OWConcurrent import *
15
16import OWGUIEx
17
18
19class ItemProgressBar(QProgressBar):
20    """Progress Bar with and `advance()` slot.
21    """
22    @pyqtSignature("advance()")
23    def advance(self):
24        self.setValue(self.value() + 1)
25
26
27class ProgressBarRedirect(QObject):
28    def __init__(self, parent, redirect):
29        QObject.__init__(self, parent)
30        self.redirect = redirect
31        self._delay = False
32
33    @pyqtSignature("advance()")
34    def advance(self):
35        # delay OWBaseWidget.progressBarSet call, because it calls
36        # qApp.processEvents which can result in 'event queue climbing'
37        # and max. recursion error if GUI thread gets another advance
38        # signal before it finishes with this one
39        if not self._delay:
40            try:
41                self._delay = True
42                self.redirect.advance()
43            finally:
44                self._delay = False
45        else:
46            QTimer.singleShot(10, self.advance)
47
48_icons_dir = os.path.join(environ.canvas_install_dir, "icons")
49
50
51def icon(name):
52    return QIcon(os.path.join(_icons_dir, name))
53
54
55class UpdateOptionsWidget(QWidget):
56    """
57    A Widget with download/update/remove options.
58    """
59    def __init__(self, updateCallback, removeCallback, state, *args):
60        QWidget.__init__(self, *args)
61        self.updateCallback = updateCallback
62        self.removeCallback = removeCallback
63        layout = QHBoxLayout()
64        layout.setSpacing(1)
65        layout.setContentsMargins(1, 1, 1, 1)
66        self.updateButton = QToolButton(self)
67        self.updateButton.setIcon(icon("update.png"))
68        self.updateButton.setToolTip("Download")
69
70        self.removeButton = QToolButton(self)
71        self.removeButton.setIcon(icon("delete.png"))
72        self.removeButton.setToolTip("Remove from system")
73
74        self.connect(self.updateButton, SIGNAL("released()"),
75                     self.updateCallback)
76        self.connect(self.removeButton, SIGNAL("released()"),
77                     self.removeCallback)
78
79        self.setMaximumHeight(30)
80        layout.addWidget(self.updateButton)
81        layout.addWidget(self.removeButton)
82        self.setLayout(layout)
83        self.SetState(state)
84
85    def SetState(self, state):
86        self.state = state
87        if state == 0:
88            self.updateButton.setIcon(icon("update1.png"))
89            self.updateButton.setToolTip("Update")
90            self.updateButton.setEnabled(False)
91            self.removeButton.setEnabled(True)
92        elif state == 1:
93            self.updateButton.setIcon(icon("update1.png"))
94            self.updateButton.setToolTip("Update")
95            self.updateButton.setEnabled(True)
96            self.removeButton.setEnabled(True)
97        elif state == 2:
98            self.updateButton.setIcon(icon("update.png"))
99            self.updateButton.setToolTip("Download")
100            self.updateButton.setEnabled(True)
101            self.removeButton.setEnabled(False)
102        elif state == 3:
103            self.updateButton.setIcon(icon("update.png"))
104            self.updateButton.setToolTip("")
105            self.updateButton.setEnabled(False)
106            self.removeButton.setEnabled(True)
107        else:
108            raise ValueError("Invalid state %r" % state)
109
110
111class UpdateTreeWidgetItem(QTreeWidgetItem):
112    stateDict = {0: "up-to-date",
113                 1: "new version available",
114                 2: "not downloaded",
115                 3: "obsolete"}
116
117    def __init__(self, master, treeWidget, domain, filename, infoLocal,
118                 infoServer, *args):
119        dateServer = dateLocal = None
120        if infoServer:
121            dateServer = datetime.strptime(
122                infoServer["datetime"].split(".")[0], "%Y-%m-%d %H:%M:%S"
123            )
124        if infoLocal:
125            dateLocal = datetime.strptime(
126                infoLocal["datetime"].split(".")[0], "%Y-%m-%d %H:%M:%S"
127            )
128        if not infoLocal:
129            self.state = 2
130        elif not infoServer:
131            self.state = 3
132        else:
133            self.state = 0 if dateLocal >= dateServer else 1
134
135        title = infoServer["title"] if infoServer else (infoLocal["title"])
136        tags = infoServer["tags"] if infoServer else infoLocal["tags"]
137        specialTags = dict([tuple(tag.split(":"))
138                            for tag in tags
139                            if tag.startswith("#") and ":" in tag])
140        tags = ", ".join(tag for tag in tags if not tag.startswith("#"))
141        self.size = infoServer["size"] if infoServer else infoLocal["size"]
142
143        size = sizeof_fmt(float(self.size))
144        state = self.stateDict[self.state]
145        if self.state == 1:
146            state += dateServer.strftime(" (%Y, %b, %d)")
147
148        QTreeWidgetItem.__init__(self, treeWidget, ["", title, size])
149        if dateServer is not None:
150            self.setData(3, Qt.DisplayRole,
151                         dateServer.date().isoformat())
152
153        self.updateWidget = UpdateOptionsWidget(
154            self.StartDownload, self.Remove, self.state, treeWidget
155        )
156
157        self.treeWidget().setItemWidget(self, 0, self.updateWidget)
158        self.updateWidget.show()
159        self.master = master
160        self.title = title
161        self.tags = tags.split(", ")
162        self.specialTags = specialTags
163        self.domain = domain
164        self.filename = filename
165        self.UpdateToolTip()
166
167    def UpdateToolTip(self):
168        state = {0: "local, updated",
169                 1: "local, needs update",
170                 2: "on server, download for local use",
171                 3: "obsolete"}
172        tooltip = "State: %s\nTags: %s" % (state[self.state],
173                                           ", ".join(self.tags))
174        if self.state != 2:
175            tooltip += ("\nFile: %s" %
176                        serverfiles.localpath(self.domain, self.filename))
177        for i in range(1, 5):
178            self.setToolTip(i, tooltip)
179
180    def StartDownload(self):
181        self.updateWidget.removeButton.setEnabled(False)
182        self.updateWidget.updateButton.setEnabled(False)
183        self.setData(2, Qt.DisplayRole, QVariant(""))
184        serverFiles = serverfiles.ServerFiles(
185            access_code=self.master.accessCode if self.master.accessCode
186            else None
187        )
188
189        pb = ItemProgressBar(self.treeWidget())
190        pb.setRange(0, 100)
191        pb.setTextVisible(False)
192
193        self.task = AsyncCall(threadPool=QThreadPool.globalInstance())
194
195        if not getattr(self.master, "_sum_progressBar", None):
196            self.master._sum_progressBar = OWGUI.ProgressBar(self.master, 0)
197            self.master._sum_progressBar.in_progress = 0
198        master_pb = self.master._sum_progressBar
199        master_pb.iter += 100
200        master_pb.in_progress += 1
201        self._progressBarRedirect = \
202            ProgressBarRedirect(QThread.currentThread(), master_pb)
203        QObject.connect(self.task,
204                        SIGNAL("advance()"),
205                        pb.advance,
206                        Qt.QueuedConnection)
207        QObject.connect(self.task,
208                        SIGNAL("advance()"),
209                        self._progressBarRedirect.advance,
210                        Qt.QueuedConnection)
211        QObject.connect(self.task,
212                        SIGNAL("finished(QString)"),
213                        self.EndDownload,
214                        Qt.QueuedConnection)
215        self.treeWidget().setItemWidget(self, 2, pb)
216        pb.show()
217
218        self.task.apply_async(serverfiles.download,
219                              args=(self.domain, self.filename, serverFiles),
220                              kwargs=dict(callback=self.task.emitAdvance))
221
222    def EndDownload(self, exitCode=0):
223        self.treeWidget().removeItemWidget(self, 2)
224        if str(exitCode) == "Ok":
225            self.state = 0
226            self.updateWidget.SetState(self.state)
227            self.setData(2, Qt.DisplayRole,
228                         QVariant(sizeof_fmt(float(self.size))))
229            self.master.UpdateInfoLabel()
230            self.UpdateToolTip()
231        else:
232            self.updateWidget.SetState(1)
233            self.setData(2, Qt.DisplayRole,
234                         QVariant("Error occurred while downloading:" +
235                                  str(exitCode)))
236
237        master_pb = self.master._sum_progressBar
238
239        if master_pb and master_pb.in_progress == 1:
240            master_pb.finish()
241            self.master._sum_progressBar = None
242        elif master_pb:
243            master_pb.in_progress -= 1
244
245    def Remove(self):
246        serverfiles.remove(self.domain, self.filename)
247        self.state = 2
248        self.updateWidget.SetState(self.state)
249        self.master.UpdateInfoLabel()
250        self.UpdateToolTip()
251
252    def __contains__(self, item):
253        return any(item.lower() in tag.lower()
254                   for tag in self.tags + [self.title])
255
256    def __lt__(self, other):
257        return getattr(self, "title", "") < getattr(other, "title", "")
258
259
260class UpdateItemDelegate(QItemDelegate):
261    def sizeHint(self, option, index):
262        size = QItemDelegate.sizeHint(self, option, index)
263        parent = self.parent()
264        item = parent.itemFromIndex(index)
265        widget = parent.itemWidget(item, 0)
266        if widget:
267            size = QSize(size.width(), widget.sizeHint().height() / 2)
268        return size
269
270
271def retrieveFilesList(serverFiles, domains=None, advance=lambda: None):
272    """
273    Retrieve and return serverfiles.allinfo for all domains.
274    """
275    domains = serverFiles.listdomains() if domains is None else domains
276    advance()
277    serverInfo = dict([(dom, serverFiles.allinfo(dom)) for dom in domains])
278    advance()
279    return serverInfo
280
281
282class OWDatabasesUpdate(OWWidget):
283    def __init__(self, parent=None, signalManager=None,
284                 name="Databases update", wantCloseButton=False,
285                 searchString="", showAll=True, domains=None,
286                 accessCode=""):
287        OWWidget.__init__(self, parent, signalManager, name)
288        self.searchString = searchString
289        self.accessCode = accessCode
290        self.showAll = showAll
291        self.domains = domains
292        self.serverFiles = serverfiles.ServerFiles()
293        box = OWGUI.widgetBox(self.mainArea, orientation="horizontal")
294
295        self.lineEditFilter = \
296            OWGUIEx.lineEditHint(box, self, "searchString", "Filter",
297                                 caseSensitive=False,
298                                 delimiters=" ",
299                                 matchAnywhere=True,
300                                 listUpdateCallback=self.SearchUpdate,
301                                 callbackOnType=True,
302                                 callback=self.SearchUpdate)
303
304        box = OWGUI.widgetBox(self.mainArea, "Files")
305        self.filesView = QTreeWidget(self)
306        self.filesView.setHeaderLabels(["Options", "Title", "Size",
307                                        "Last Updated"])
308        self.filesView.setRootIsDecorated(False)
309        self.filesView.setSelectionMode(QAbstractItemView.NoSelection)
310        self.filesView.setSortingEnabled(True)
311        self.filesView.setItemDelegate(UpdateItemDelegate(self.filesView))
312        self.connect(self.filesView.model(),
313                     SIGNAL("layoutChanged()"),
314                     self.SearchUpdate)
315        box.layout().addWidget(self.filesView)
316
317        box = OWGUI.widgetBox(self.mainArea, orientation="horizontal")
318        OWGUI.button(box, self, "Update all local files",
319                     callback=self.UpdateAll,
320                     tooltip="Update all updatable files")
321        OWGUI.button(box, self, "Download filtered",
322                     callback=self.DownloadFiltered,
323                     tooltip="Download all filtered files shown")
324        OWGUI.rubber(box)
325        OWGUI.lineEdit(box, self, "accessCode", "Access Code",
326                       orientation="horizontal",
327                       callback=self.RetrieveFilesList)
328        self.retryButton = OWGUI.button(box, self, "Retry",
329                                        callback=self.RetrieveFilesList)
330        self.retryButton.hide()
331        box = OWGUI.widgetBox(self.mainArea, orientation="horizontal")
332        OWGUI.rubber(box)
333        if wantCloseButton:
334            OWGUI.button(box, self, "Close",
335                         callback=self.accept,
336                         tooltip="Close")
337
338        self.infoLabel = QLabel()
339        self.infoLabel.setAlignment(Qt.AlignCenter)
340
341        self.mainArea.layout().addWidget(self.infoLabel)
342        self.infoLabel.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
343
344        self.updateItems = []
345        self.allTags = []
346
347        self.resize(800, 600)
348
349        QTimer.singleShot(50, self.RetrieveFilesList)
350
351    def RetrieveFilesList(self):
352        self.serverFiles = serverfiles.ServerFiles(access_code=self.accessCode)
353        self.pb = ProgressBar(self, 3)
354        self.async_retrieve = createTask(retrieveFilesList,
355                                         (self.serverFiles, self.domains,
356                                          self.pb.advance),
357                                         onResult=self.SetFilesList,
358                                         onError=self.HandleError)
359
360        self.setEnabled(False)
361
362    def SetFilesList(self, serverInfo):
363        self.setEnabled(True)
364        domains = serverInfo.keys() or serverfiles.listdomains()
365        localInfo = dict([(dom, serverfiles.allinfo(dom))
366                          for dom in domains])
367        items = []
368
369        self.allTags = set()
370        allTitles = set()
371        self.updateItems = []
372
373        for domain in set(domains) - set(["test", "demo"]):
374            local = localInfo.get(domain, {})
375            server = serverInfo.get(domain, {})
376            files = sorted(set(server.keys() + local.keys()))
377            for filename in files:
378                infoServer = server.get(filename, None)
379                infoLocal = local.get(filename, None)
380
381                items.append((self.filesView, domain, filename, infoLocal,
382                              infoServer))
383
384                displayInfo = infoServer if infoServer else infoLocal
385                self.allTags.update(displayInfo["tags"])
386                allTitles.update(displayInfo["title"].split())
387
388        for item in items:
389            self.updateItems.append(UpdateTreeWidgetItem(self, *item))
390        self.pb.advance()
391        for column in range(4):
392            whint = self.filesView.sizeHintForColumn(column)
393            width = min(whint, 400)
394            self.filesView.setColumnWidth(column, width)
395
396        self.lineEditFilter.setItems([hint for hint in sorted(self.allTags)
397                                      if not hint.startswith("#")])
398        self.SearchUpdate()
399        self.UpdateInfoLabel()
400        self.pb.finish()
401
402    def HandleError(self, (exc_type, exc_value, tb)):
403        if exc_type >= IOError:
404            self.error(0,
405                       "Could not connect to server! Press the Retry "
406                       "button to try again.")
407            self.SetFilesList({})
408        else:
409            sys.excepthook(exc_type, exc_value, tb)
410            self.pb.finish()
411            self.setEnabled(True)
412
413    def UpdateInfoLabel(self):
414        local = [item for item in self.updateItems if item.state != 2]
415        onServer = [item for item in self.updateItems]
416        size = sum(float(item.specialTags.get("#uncompressed", item.size))
417                   for item in local)
418        sizeOnServer = sum(float(item.size) for item in self.updateItems)
419
420        if self.showAll:
421
422            text = ("%i items, %s (data on server: %i items, %s)" %
423                    (len(local),
424                     sizeof_fmt(size),
425                     len(onServer),
426                     sizeof_fmt(sizeOnServer)))
427        else:
428            text = "%i items, %s" % (len(local), sizeof_fmt(size))
429
430        self.infoLabel.setText(text)
431
432    def UpdateAll(self):
433        for item in self.updateItems:
434            if item.state == 1:
435                item.StartDownload()
436
437    def DownloadFiltered(self):
438        for item in self.updateItems:
439            if not item.isHidden() and item.state != 0:
440                item.StartDownload()
441
442    def SearchUpdate(self, searchString=None):
443        strings = unicode(self.lineEditFilter.text()).split()
444        tags = set()
445        for item in self.updateItems:
446            hide = not all(str(string) in item for string in strings)
447            item.setHidden(hide)
448            if not hide:
449                tags.update(item.tags)
450
451
452if __name__ == "__main__":
453    app = QApplication(sys.argv)
454    w = OWDatabasesUpdate(wantCloseButton=True)
455    w.show()
456    w.exec_()
Note: See TracBrowser for help on using the repository browser.