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'

RevLine 
[11436]1from __future__ import with_statement
2
3import os
4import sys
5
6from datetime import datetime
7
[11437]8import Orange
[11436]9
[11437]10from Orange.utils import serverfiles, environ
[11436]11from Orange.utils.serverfiles import sizeformat as sizeof_fmt
12
[8042]13from OWWidget import *
[11436]14from OWConcurrent import *
[8042]15
[11436]16import OWGUIEx
[8042]17
18
19class ItemProgressBar(QProgressBar):
[11437]20    """Progress Bar with and `advance()` slot.
[11436]21    """
[8042]22    @pyqtSignature("advance()")
23    def advance(self):
24        self.setValue(self.value() + 1)
[11436]25
26
[8042]27class ProgressBarRedirect(QObject):
28    def __init__(self, parent, redirect):
29        QObject.__init__(self, parent)
30        self.redirect = redirect
31        self._delay = False
[11436]32
[8042]33    @pyqtSignature("advance()")
34    def advance(self):
[11436]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
[8042]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
[11437]48_icons_dir = os.path.join(environ.canvas_install_dir, "icons")
[11436]49
50
51def icon(name):
52    return QIcon(os.path.join(_icons_dir, name))
53
54
[8042]55class UpdateOptionsWidget(QWidget):
[11437]56    """
57    A Widget with download/update/remove options.
58    """
[8042]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)
[11436]67        self.updateButton.setIcon(icon("update.png"))
[8042]68        self.updateButton.setToolTip("Download")
[11436]69
[8042]70        self.removeButton = QToolButton(self)
[11436]71        self.removeButton.setIcon(icon("delete.png"))
[8042]72        self.removeButton.setToolTip("Remove from system")
[11436]73
74        self.connect(self.updateButton, SIGNAL("released()"),
75                     self.updateCallback)
76        self.connect(self.removeButton, SIGNAL("released()"),
77                     self.removeCallback)
78
[8042]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:
[11436]88            self.updateButton.setIcon(icon("update1.png"))
[8042]89            self.updateButton.setToolTip("Update")
90            self.updateButton.setEnabled(False)
91            self.removeButton.setEnabled(True)
92        elif state == 1:
[11436]93            self.updateButton.setIcon(icon("update1.png"))
[8042]94            self.updateButton.setToolTip("Update")
95            self.updateButton.setEnabled(True)
96            self.removeButton.setEnabled(True)
97        elif state == 2:
[11436]98            self.updateButton.setIcon(icon("update.png"))
[8042]99            self.updateButton.setToolTip("Download")
100            self.updateButton.setEnabled(True)
101            self.removeButton.setEnabled(False)
102        elif state == 3:
[11436]103            self.updateButton.setIcon(icon("update.png"))
[8042]104            self.updateButton.setToolTip("")
105            self.updateButton.setEnabled(False)
106            self.removeButton.setEnabled(True)
[11437]107        else:
108            raise ValueError("Invalid state %r" % state)
[8042]109
110
111class UpdateTreeWidgetItem(QTreeWidgetItem):
[11436]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):
[11437]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            )
[8042]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
[11436]134
[8042]135        title = infoServer["title"] if infoServer else (infoLocal["title"])
136        tags = infoServer["tags"] if infoServer else infoLocal["tags"]
[11436]137        specialTags = dict([tuple(tag.split(":"))
138                            for tag in tags
139                            if tag.startswith("#") and ":" in tag])
[8042]140        tags = ", ".join(tag for tag in tags if not tag.startswith("#"))
141        self.size = infoServer["size"] if infoServer else infoLocal["size"]
[11436]142
[8042]143        size = sizeof_fmt(float(self.size))
[11436]144        state = self.stateDict[self.state]
145        if self.state == 1:
146            state += dateServer.strftime(" (%Y, %b, %d)")
147
[8042]148        QTreeWidgetItem.__init__(self, treeWidget, ["", title, size])
[11437]149        if dateServer is not None:
150            self.setData(3, Qt.DisplayRole,
151                         dateServer.date().isoformat())
152
[11436]153        self.updateWidget = UpdateOptionsWidget(
154            self.StartDownload, self.Remove, self.state, treeWidget
155        )
156
[8042]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):
[11436]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))
[8042]174        if self.state != 2:
[11437]175            tooltip += ("\nFile: %s" %
176                        serverfiles.localpath(self.domain, self.filename))
[8042]177        for i in range(1, 5):
178            self.setToolTip(i, tooltip)
[11436]179
[8042]180    def StartDownload(self):
181        self.updateWidget.removeButton.setEnabled(False)
182        self.updateWidget.updateButton.setEnabled(False)
183        self.setData(2, Qt.DisplayRole, QVariant(""))
[11437]184        serverFiles = serverfiles.ServerFiles(
[11436]185            access_code=self.master.accessCode if self.master.accessCode
186            else None
187        )
188
[8042]189        pb = ItemProgressBar(self.treeWidget())
190        pb.setRange(0, 100)
191        pb.setTextVisible(False)
[11436]192
[8042]193        self.task = AsyncCall(threadPool=QThreadPool.globalInstance())
[11436]194
[8042]195        if not getattr(self.master, "_sum_progressBar", None):
[11436]196            self.master._sum_progressBar = OWGUI.ProgressBar(self.master, 0)
[8042]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
[11436]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)
[8042]215        self.treeWidget().setItemWidget(self, 2, pb)
216        pb.show()
[11436]217
[11437]218        self.task.apply_async(serverfiles.download,
[11436]219                              args=(self.domain, self.filename, serverFiles),
220                              kwargs=dict(callback=self.task.emitAdvance))
[8042]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)
[11436]227            self.setData(2, Qt.DisplayRole,
228                         QVariant(sizeof_fmt(float(self.size))))
[8042]229            self.master.UpdateInfoLabel()
230            self.UpdateToolTip()
231        else:
232            self.updateWidget.SetState(1)
[11436]233            self.setData(2, Qt.DisplayRole,
234                         QVariant("Error occurred while downloading:" +
235                                  str(exitCode)))
236
[8042]237        master_pb = self.master._sum_progressBar
[11436]238
[8042]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
[11436]244
[8042]245    def Remove(self):
[11437]246        serverfiles.remove(self.domain, self.filename)
[8042]247        self.state = 2
248        self.updateWidget.SetState(self.state)
249        self.master.UpdateInfoLabel()
250        self.UpdateToolTip()
251
252    def __contains__(self, item):
[11436]253        return any(item.lower() in tag.lower()
254                   for tag in self.tags + [self.title])
255
[9305]256    def __lt__(self, other):
[11436]257        return getattr(self, "title", "") < getattr(other, "title", "")
258
[8042]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:
[11436]267            size = QSize(size.width(), widget.sizeHint().height() / 2)
[8042]268        return size
[11436]269
270
[8042]271def retrieveFilesList(serverFiles, domains=None, advance=lambda: None):
[11436]272    """
273    Retrieve and return serverfiles.allinfo for all domains.
274    """
[8042]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
[11436]280
281
[8042]282class OWDatabasesUpdate(OWWidget):
[11436]283    def __init__(self, parent=None, signalManager=None,
284                 name="Databases update", wantCloseButton=False,
285                 searchString="", showAll=True, domains=None,
286                 accessCode=""):
[8042]287        OWWidget.__init__(self, parent, signalManager, name)
288        self.searchString = searchString
289        self.accessCode = accessCode
290        self.showAll = showAll
291        self.domains = domains
[11437]292        self.serverFiles = serverfiles.ServerFiles()
[8042]293        box = OWGUI.widgetBox(self.mainArea, orientation="horizontal")
[11436]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)
[8042]303
304        box = OWGUI.widgetBox(self.mainArea, "Files")
305        self.filesView = QTreeWidget(self)
[11437]306        self.filesView.setHeaderLabels(["Options", "Title", "Size",
307                                        "Last Updated"])
[8042]308        self.filesView.setRootIsDecorated(False)
309        self.filesView.setSelectionMode(QAbstractItemView.NoSelection)
310        self.filesView.setSortingEnabled(True)
311        self.filesView.setItemDelegate(UpdateItemDelegate(self.filesView))
[11436]312        self.connect(self.filesView.model(),
313                     SIGNAL("layoutChanged()"),
314                     self.SearchUpdate)
[8042]315        box.layout().addWidget(self.filesView)
316
317        box = OWGUI.widgetBox(self.mainArea, orientation="horizontal")
[11436]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")
[8042]324        OWGUI.rubber(box)
[11436]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)
[8042]330        self.retryButton.hide()
331        box = OWGUI.widgetBox(self.mainArea, orientation="horizontal")
332        OWGUI.rubber(box)
333        if wantCloseButton:
[11436]334            OWGUI.button(box, self, "Close",
335                         callback=self.accept,
336                         tooltip="Close")
[8042]337
338        self.infoLabel = QLabel()
339        self.infoLabel.setAlignment(Qt.AlignCenter)
[11436]340
[8042]341        self.mainArea.layout().addWidget(self.infoLabel)
342        self.infoLabel.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
343
344        self.updateItems = []
345        self.allTags = []
[11436]346
[8042]347        self.resize(800, 600)
[11436]348
[8042]349        QTimer.singleShot(50, self.RetrieveFilesList)
[11436]350
[8042]351    def RetrieveFilesList(self):
[11437]352        self.serverFiles = serverfiles.ServerFiles(access_code=self.accessCode)
[8042]353        self.pb = ProgressBar(self, 3)
[11436]354        self.async_retrieve = createTask(retrieveFilesList,
355                                         (self.serverFiles, self.domains,
356                                          self.pb.advance),
357                                         onResult=self.SetFilesList,
358                                         onError=self.HandleError)
359
[8042]360        self.setEnabled(False)
[11436]361
[8042]362    def SetFilesList(self, serverInfo):
363        self.setEnabled(True)
[11437]364        domains = serverInfo.keys() or serverfiles.listdomains()
365        localInfo = dict([(dom, serverfiles.allinfo(dom))
[11436]366                          for dom in domains])
[8042]367        items = []
[11436]368
[8042]369        self.allTags = set()
370        allTitles = set()
371        self.updateItems = []
[11436]372
373        for domain in set(domains) - set(["test", "demo"]):
374            local = localInfo.get(domain, {})
375            server = serverInfo.get(domain, {})
[8042]376            files = sorted(set(server.keys() + local.keys()))
[11436]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
[8042]384                displayInfo = infoServer if infoServer else infoLocal
385                self.allTags.update(displayInfo["tags"])
386                allTitles.update(displayInfo["title"].split())
[11436]387
388        for item in items:
[8042]389            self.updateItems.append(UpdateTreeWidgetItem(self, *item))
390        self.pb.advance()
[11437]391        for column in range(4):
392            whint = self.filesView.sizeHintForColumn(column)
393            width = min(whint, 400)
394            self.filesView.setColumnWidth(column, width)
395
[11436]396        self.lineEditFilter.setItems([hint for hint in sorted(self.allTags)
397                                      if not hint.startswith("#")])
[8042]398        self.SearchUpdate()
399        self.UpdateInfoLabel()
400        self.pb.finish()
[11436]401
[8042]402    def HandleError(self, (exc_type, exc_value, tb)):
403        if exc_type >= IOError:
[11436]404            self.error(0,
405                       "Could not connect to server! Press the Retry "
406                       "button to try again.")
[8042]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]
[11436]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
[8042]420        if self.showAll:
[11436]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)))
[8042]427        else:
[11436]428            text = "%i items, %s" % (len(local), sizeof_fmt(size))
429
430        self.infoLabel.setText(text)
431
[8042]432    def UpdateAll(self):
433        for item in self.updateItems:
434            if item.state == 1:
435                item.StartDownload()
[11436]436
[8042]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):
[11436]443        strings = unicode(self.lineEditFilter.text()).split()
[8042]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)
[11436]450
451
[8042]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.