source: orange/Orange/OrangeWidgets/OWDatabasesUpdate.py @ 11693:56d368674f01

Revision 11693:56d368674f01, 27.3 KB checked in by markotoplak, 7 months ago (diff)

Database update redesign (changes by Vid).

RevLine 
[11436]1from __future__ import with_statement
2
3import os
4import sys
5
[11693]6from datetime import datetime, timedelta
[11483]7from functools import partial
[11484]8from collections import namedtuple
[11436]9
[11483]10from PyQt4.QtCore import pyqtSignal as Signal, pyqtSlot as Slot
[11436]11
[11437]12from Orange.utils import serverfiles, environ
[11436]13from Orange.utils.serverfiles import sizeformat as sizeof_fmt
14
[8042]15from OWWidget import *
[11483]16
17from OWConcurrent import Task, ThreadExecutor, methodinvoke
[8042]18
[11436]19import OWGUIEx
[11693]20import OWGUI
[8042]21
22
[11484]23#: Update file item states
24AVAILABLE, CURRENT, OUTDATED, DEPRECATED = range(4)
25
26_icons_dir = os.path.join(environ.canvas_install_dir, "icons")
27
28
29def icon(name):
30    return QIcon(os.path.join(_icons_dir, name))
31
32
[8042]33class ItemProgressBar(QProgressBar):
[11437]34    """Progress Bar with and `advance()` slot.
[11436]35    """
[11484]36    @Slot()
[8042]37    def advance(self):
[11484]38        """
39        Advance the progress bar by 1
40        """
[8042]41        self.setValue(self.value() + 1)
[11436]42
43
[11484]44class UpdateOptionButton(QToolButton):
45    def event(self, event):
46        if event.type() == QEvent.Wheel:
47            # QAbstractButton automatically accepts all mouse events (in
48            # event method) for disabled buttons. This can prevent scrolling
49            # in a scroll area when a disabled button scrolls under the
50            # mouse.
51            event.ignore()
52            return False
53        else:
54            return QToolButton.event(self, event)
[11436]55
56
[8042]57class UpdateOptionsWidget(QWidget):
[11437]58    """
59    A Widget with download/update/remove options.
60    """
[11484]61    #: Install/update button was clicked
62    installClicked = Signal()
63    #: Remove button was clicked.
64    removeClicked = Signal()
[11693]65   
[11484]66    def __init__(self, state=AVAILABLE, parent=None):
67        QWidget.__init__(self, parent)
[8042]68        layout = QHBoxLayout()
69        layout.setSpacing(1)
70        layout.setContentsMargins(1, 1, 1, 1)
[11436]71
[11693]72        self.checkButton = QCheckBox()
73         
74        layout.addWidget(self.checkButton)
[11484]75        self.setLayout(layout)
[11693]76 
[8042]77        self.setMaximumHeight(30)
78
[11484]79        self.state = -1
80        self.setState(state)
[11693]81       
[11484]82    def setState(self, state):
83        """
84        Set the current update state for the widget (AVAILABLE,
85        CURRENT, OUTDATED or DEPRECTED).
86
87        """
88        if self.state != state:
89            self.state = state
90            self._update()
[11693]91 
[11484]92    def _update(self):
93        if self.state == AVAILABLE:
[11693]94            self.checkButton.setChecked(False)
[11484]95        elif self.state == CURRENT:
[11693]96            self.checkButton.setChecked(True)
[11484]97        elif self.state == OUTDATED:
[11693]98            self.checkButton.setChecked(True)
[11484]99        elif self.state == DEPRECATED:
[11693]100            self.checkButton.setChecked(True)
[11437]101        else:
[11484]102            raise ValueError("Invalid state %r" % self._state)
[8042]103
[11693]104        try:
105            self.checkButton.clicked.disconnect()   # Remove old signals if they exist
106        except:
107            pass
108
109        if not self.checkButton.isChecked():        # Switch signals if the file is present or not
110            self.checkButton.clicked.connect(self.installClicked)
111        else:
112            self.checkButton.clicked.connect(self.removeClicked)
113
[8042]114
115class UpdateTreeWidgetItem(QTreeWidgetItem):
[11484]116    """
117    A QTreeWidgetItem for displaying an UpdateItem.
[11436]118
[11484]119    :param UpdateItem item:
120        The update item for display.
121
122    """
123    STATE_STRINGS = {0: "not downloaded",
124                     1: "downloaded, current",
125                     2: "downloaded, needs update",
126                     3: "obsolete"}
127
128    #: A role for the state item data.
129    StateRole = OWGUI.OrangeUserRole.next()
130
131    # QTreeWidgetItem stores the DisplayRole and EditRole as the same role,
132    # so we can't use EditRole to store the actual item data, instead we use
133    # custom role.
134
135    #: A custom edit role for the item's data
136    EditRole2 = OWGUI.OrangeUserRole.next()
137
138    def __init__(self, item):
139        QTreeWidgetItem.__init__(self, type=QTreeWidgetItem.UserType)
140
141        self.item = None
142        self.setUpdateItem(item)
143
144    def setUpdateItem(self, item):
145        """
146        Set the update item for display.
147
148        :param UpdateItem item:
149            The update item for display.
150
151        """
152        self.item = item
153
154        self.setData(0, UpdateTreeWidgetItem.StateRole, item.state)
155
156        self.setData(1, Qt.DisplayRole, item.title)
157        self.setData(1, self.EditRole2, item.title)
158
[11693]159        self.setData(4, Qt.DisplayRole, sizeof_fmt(item.size))
160        self.setData(4, self.EditRole2, item.size)
[11484]161
[11693]162        if item.local is not None:
163            self.setData(3, Qt.DisplayRole, item.local.date().isoformat())
164            self.setData(3, self.EditRole2, item.local)
[8042]165        else:
[11693]166            self.setData(3, Qt.DisplayRole, "")
[11513]167            self.setData(3, self.EditRole2, datetime.now())
[11436]168
[11484]169        self._updateToolTip()
[11436]170
[11484]171    def _updateToolTip(self):
172        state_str = self.STATE_STRINGS[self.item.state]
[11693]173        try:
174            diff_date = self.item.latest - self.item.local
175        except:
176            diff_date = None
177       
[11484]178        tooltip = ("State: %s\nTags: %s" %
[11693]179                   (state_str, ", ".join(tag for tag in self.item.tags
180                    if not tag.startswith("#"))))
[11436]181
[11484]182        if self.item.state in [CURRENT, OUTDATED, DEPRECATED]:
[11437]183            tooltip += ("\nFile: %s" %
[11484]184                        serverfiles.localpath(self.item.domain,
185                                              self.item.filename))
[11693]186       
187        if self.item.state == 2 and diff_date:
188            tooltip += ("\nServer version: %s\nStatus: old (%d days)" % (self.item.latest, diff_date.days))
189        else:
190            tooltip += ("\nServer version: %s" % self.item.latest)
191
[11484]192        for i in range(1, 4):
[8042]193            self.setToolTip(i, tooltip)
[11436]194
[11484]195    def __lt__(self, other):
196        widget = self.treeWidget()
197        column = widget.sortColumn()
198        if column == 0:
199            role = UpdateTreeWidgetItem.StateRole
200        else:
201            role = self.EditRole2
[11436]202
[11484]203        left = self.data(column, role).toPyObject()
204        right = other.data(column, role).toPyObject()
205        return left < right
[8042]206
[11436]207
[11484]208class UpdateOptionsItemDelegate(QStyledItemDelegate):
209    """
210    An item delegate for the updates tree widget.
[11436]211
[11484]212    .. note: Must be a child of a QTreeWidget.
[8042]213
[11484]214    """
[8042]215    def sizeHint(self, option, index):
[11484]216        size = QStyledItemDelegate.sizeHint(self,  option, index)
[8042]217        parent = self.parent()
218        item = parent.itemFromIndex(index)
219        widget = parent.itemWidget(item, 0)
220        if widget:
[11436]221            size = QSize(size.width(), widget.sizeHint().height() / 2)
[8042]222        return size
[11436]223
224
[11484]225UpdateItem = namedtuple(
226    "UpdateItem",
227    ["domain",
228     "filename",
229     "state",  # Item state flag
230     "title",  # Item title (on server is available else local)
231     "size",  # Item size in bytes (on server if available else local)
232     "latest",  # Latest item date (on server), can be None
233     "local",  # Local item date, can be None
234     "tags",  # Item tags (on server if available else local)
235     "info_local",
236     "info_server"]
237)
238
239ItemInfo = namedtuple(
240    "ItemInfo",
241    ["domain",
242     "filename",
243     "title",
244     "time",  # datetime.datetime
245     "size",  # size in bytes
246     "tags"]
247)
248
249
250def UpdateItem_match(item, string):
251    """
252    Return `True` if the `UpdateItem` item contains a string in tags
253    or in the title.
254
255    """
256    string = string.lower()
257    return any(string.lower() in tag.lower()
258               for tag in item.tags + [item.title])
259
260
261def item_state(info_local, info_server):
262    """
263    Return the item state (AVAILABLE, ...) based on it's local and server side
264    `ItemInfo` instances.
265
266    """
267    if info_server is None:
268        return DEPRECATED
269
270    if info_local is None:
271        return AVAILABLE
272
273    if info_local.time < info_server.time:
274        return OUTDATED
275    else:
276        return CURRENT
277
278
279DATE_FMT_1 = "%Y-%m-%d %H:%M:%S.%f"
280DATE_FMT_2 = "%Y-%m-%d %H:%M:%S"
281
282
283def info_dict_to_item_info(domain, filename, item_dict):
284    """
285    Return an `ItemInfo` instance based on `item_dict` as returned by
286    ``serverfiles.info(domain, filename)``
287
288    """
289    time = item_dict["datetime"]
290    try:
291        time = datetime.strptime(time, DATE_FMT_1)
292    except ValueError:
293        time = datetime.strptime(time, DATE_FMT_2)
294
295    title = item_dict["title"]
296    if not title:
297        title = filename
298
299    size = int(item_dict["size"])
300    tags = item_dict["tags"]
301    return ItemInfo(domain, filename, title, time, size, tags)
302
303
304def update_item_from_info(domain, filename, info_server, info_local):
305    """
306    Return a `UpdateItem` instance for `domain`, `fileanme` based on
307    the local and server side `ItemInfo` instances `info_server` and
308    `info_local`.
309
310    """
311    latest, local, title, tags, size = None, None, None, None, None
312
313    if info_server is not None:
314        info_server = info_dict_to_item_info(domain, filename, info_server)
315        latest = info_server.time
316        tags = info_server.tags
317        title = info_server.title
318        size = info_server.size
319
320    if info_local is not None:
321        info_local = info_dict_to_item_info(domain, filename, info_local)
322        local = info_local.time
323
324        if info_server is None:
325            tags = info_local.tags
326            title = info_local.title
327            size = info_local.size
328
329    state = item_state(info_local, info_server)
330
331    return UpdateItem(domain, filename, state, title, size, latest, local,
332                      tags, info_server, info_local)
333
334
335def join_info_list(domain, files_local, files_server):
336    filenames = set(files_local.keys()).union(files_server.keys())
337    for filename in sorted(filenames):
338        info_server = files_server.get(filename, None)
339        info_local = files_local.get(filename, None)
340        yield update_item_from_info(domain, filename, info_server, info_local)
341
342
343def join_info_dict(local, server):
344    domains = set(local.keys()).union(server.keys())
345    for domain in sorted(domains):
346        files_local = local.get(domain, {})
347        files_server = server.get(domain, {})
348
349        for item in join_info_list(domain, files_local, files_server):
350            yield item
351
352
353def special_tags(item):
354    """
355    Return a dictionary of special tags in an UpdateItem instance (special
356    tags are the ones starting with #).
357
358    """
359    return dict([tuple(tag.split(":")) for tag in item.tags
360                 if tag.startswith("#") and ":" in tag])
361
362
[8042]363def retrieveFilesList(serverFiles, domains=None, advance=lambda: None):
[11436]364    """
365    Retrieve and return serverfiles.allinfo for all domains.
366    """
[8042]367    domains = serverFiles.listdomains() if domains is None else domains
368    advance()
[11514]369    serverInfo = {}
370    for dom in domains:
371        try:
372            serverInfo[dom] = serverFiles.allinfo(dom)
373        except Exception: #ignore inexistent domains
374            pass
[8042]375    advance()
376    return serverInfo
[11436]377
378
[8042]379class OWDatabasesUpdate(OWWidget):
[11436]380    def __init__(self, parent=None, signalManager=None,
381                 name="Databases update", wantCloseButton=False,
382                 searchString="", showAll=True, domains=None,
383                 accessCode=""):
[11484]384        OWWidget.__init__(self, parent, signalManager, name, wantMainArea=False)
[8042]385        self.searchString = searchString
386        self.accessCode = accessCode
387        self.showAll = showAll
388        self.domains = domains
[11437]389        self.serverFiles = serverfiles.ServerFiles()
[11484]390
391        box = OWGUI.widgetBox(self.controlArea, orientation="horizontal")
[11436]392
393        self.lineEditFilter = \
394            OWGUIEx.lineEditHint(box, self, "searchString", "Filter",
395                                 caseSensitive=False,
396                                 delimiters=" ",
397                                 matchAnywhere=True,
398                                 listUpdateCallback=self.SearchUpdate,
399                                 callbackOnType=True,
400                                 callback=self.SearchUpdate)
[8042]401
[11484]402        box = OWGUI.widgetBox(self.controlArea, "Files")
[8042]403        self.filesView = QTreeWidget(self)
[11693]404        self.filesView.setHeaderLabels(["", "Data Source", "Update",
405                                        "Last Updated", "Size"])
[8042]406        self.filesView.setRootIsDecorated(False)
[11484]407        self.filesView.setUniformRowHeights(True)
[8042]408        self.filesView.setSelectionMode(QAbstractItemView.NoSelection)
409        self.filesView.setSortingEnabled(True)
[11484]410        self.filesView.sortItems(1, Qt.AscendingOrder)
411        self.filesView.setItemDelegateForColumn(
412            0, UpdateOptionsItemDelegate(self.filesView))
413
414        QObject.connect(self.filesView.model(),
415                        SIGNAL("layoutChanged()"),
416                        self.SearchUpdate)
[8042]417        box.layout().addWidget(self.filesView)
418
[11484]419        box = OWGUI.widgetBox(self.controlArea, orientation="horizontal")
[11693]420        self.updateButton = OWGUI.button(box, self, "Update all",
[11436]421                     callback=self.UpdateAll,
[11693]422                     tooltip="Update all updatable files",
423                     )
424       
425        self.downloadButton = OWGUI.button(box, self, "Download all",
[11436]426                     callback=self.DownloadFiltered,
427                     tooltip="Download all filtered files shown")
[11693]428        self.cancelButton = OWGUI.button(box, self, "Cancel", callback=self.Cancel,
[11483]429                     tooltip="Cancel scheduled downloads/updates.")
[8042]430        OWGUI.rubber(box)
[11436]431        OWGUI.lineEdit(box, self, "accessCode", "Access Code",
432                       orientation="horizontal",
433                       callback=self.RetrieveFilesList)
434        self.retryButton = OWGUI.button(box, self, "Retry",
435                                        callback=self.RetrieveFilesList)
[8042]436        self.retryButton.hide()
[11484]437        box = OWGUI.widgetBox(self.controlArea, orientation="horizontal")
[8042]438        OWGUI.rubber(box)
439        if wantCloseButton:
[11436]440            OWGUI.button(box, self, "Close",
441                         callback=self.accept,
442                         tooltip="Close")
[8042]443
444        self.infoLabel = QLabel()
445        self.infoLabel.setAlignment(Qt.AlignCenter)
[11436]446
[11484]447        self.controlArea.layout().addWidget(self.infoLabel)
[8042]448        self.infoLabel.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
449
450        self.updateItems = []
[11436]451
[8042]452        self.resize(800, 600)
[11436]453
[11483]454        self.progress = ProgressState(self, maximum=3)
455        self.progress.valueChanged.connect(self._updateProgress)
456        self.progress.rangeChanged.connect(self._updateProgress)
457        self.executor = ThreadExecutor(
458            threadPool=QThreadPool(maxThreadCount=2)
459        )
460
461        task = Task(self, function=self.RetrieveFilesList)
462        task.exceptionReady.connect(self.HandleError)
463        task.start()
464
465        self._tasks = []
[11505]466        self._haveProgress = False
[11436]467
[8042]468    def RetrieveFilesList(self):
[11483]469        self.progress.setRange(0, 3)
[11437]470        self.serverFiles = serverfiles.ServerFiles(access_code=self.accessCode)
[11483]471
472        task = Task(function=partial(retrieveFilesList, self.serverFiles,
473                                     self.domains,
474                                     methodinvoke(self.progress, "advance")))
475
476        task.resultReady.connect(self.SetFilesList)
477        task.exceptionReady.connect(self.HandleError)
478
479        self.executor.submit(task)
[11436]480
[8042]481        self.setEnabled(False)
[11436]482
[11693]483
[8042]484    def SetFilesList(self, serverInfo):
[11484]485        """
486        Set the files to show.
487        """
[8042]488        self.setEnabled(True)
[11436]489
[11484]490        domains = serverInfo.keys()
491        if not domains:
492            if self.domains:
493                domains = self.domains
494            else:
495                domains = serverfiles.listdomains()
496
497        localInfo = dict([(dom, serverfiles.allinfo(dom)) for dom in domains])
498
499        all_tags = set()
500
501        self.filesView.clear()
[8042]502        self.updateItems = []
[11436]503
[11484]504        for item in join_info_dict(localInfo, serverInfo):
505            tree_item = UpdateTreeWidgetItem(item)
506            options_widget = UpdateOptionsWidget(item.state)
507            options_widget.item = item
[11436]508
[11484]509            options_widget.installClicked.connect(
510                partial(self.SubmitDownloadTask, item.domain, item.filename)
511            )
512            options_widget.removeClicked.connect(
513                partial(self.SubmitRemoveTask, item.domain, item.filename)
514            )
[11436]515
[11484]516            self.updateItems.append((item, tree_item, options_widget))
517            all_tags.update(item.tags)
[11436]518
[11484]519        self.filesView.addTopLevelItems(
520            [tree_item for _, tree_item, _ in self.updateItems]
521        )
522
523        for item, tree_item, options_widget in self.updateItems:
524            self.filesView.setItemWidget(tree_item, 0, options_widget)
[11693]525           
526            # Add an update button if the file is updateable
527            if item.state == 2:
528                ButtonWidget = QPushButton("Update")
529                layout = QHBoxLayout()
530                layout.setSpacing(1)
531                layout.setContentsMargins(20, 30, 30, 30)
532
533                layout.addWidget(ButtonWidget)                 
534                ButtonWidget.setMaximumHeight(30)
535                ButtonWidget.setMaximumWidth(120)
536                ButtonWidget.setAutoDefault(False)
537
538                ButtonWidget.clicked.connect(partial(self.SubmitDownloadTask, item.domain, item.filename))
539
540                self.filesView.setItemWidget(tree_item, 2, ButtonWidget)
[11484]541
[11483]542        self.progress.advance()
543
[11437]544        for column in range(4):
545            whint = self.filesView.sizeHintForColumn(column)
546            width = min(whint, 400)
547            self.filesView.setColumnWidth(column, width)
548
[11484]549        self.lineEditFilter.setItems([hint for hint in sorted(all_tags)
[11436]550                                      if not hint.startswith("#")])
[8042]551        self.SearchUpdate()
552        self.UpdateInfoLabel()
[11693]553        self.toggleButtons()
554        self.cancelButton.setEnabled(False)
[11436]555
[11483]556        self.progress.setRange(0, 0)
557
[11693]558    def buttonCheck(self, selected_items, state, button):
559        for item in selected_items:
560            if item.state != state:
561                button.setEnabled(False)
562            else:
563                button.setEnabled(True)
564                break
565
566    def toggleButtons(self):
567        selected_items = [item for item, tree_item, _ in self.updateItems if not tree_item.isHidden()]
568        self.buttonCheck(selected_items, OUTDATED, self.updateButton)
569        self.buttonCheck(selected_items, AVAILABLE, self.downloadButton)
570
[11483]571    def HandleError(self, exception):
572        if isinstance(exception, IOError):
[11436]573            self.error(0,
574                       "Could not connect to server! Press the Retry "
575                       "button to try again.")
[8042]576            self.SetFilesList({})
577        else:
[11483]578            sys.excepthook(type(exception), exception.args, None)
579            self.progress.setRange(0, 0)
[8042]580            self.setEnabled(True)
581
582    def UpdateInfoLabel(self):
[11619]583        local = [item for item, tree_item, _ in self.updateItems
584                 if item.state != AVAILABLE and not tree_item.isHidden() ]
585        size = sum(float(item.size) for item in local)
[11484]586
[11619]587        onServer = [item for item, tree_item, _ in self.updateItems if not tree_item.isHidden()]
588        sizeOnServer = sum(float(item.size) for item in onServer)
[11484]589
[11619]590        text = ("%i items, %s (on server: %i items, %s)" %
591                (len(local),
592                 sizeof_fmt(size),
593                 len(onServer),
594                 sizeof_fmt(sizeOnServer)))
[11436]595
596        self.infoLabel.setText(text)
597
[8042]598    def UpdateAll(self):
[11693]599        for item, tree_item, _ in self.updateItems:
600            if item.state == OUTDATED and not tree_item.isHidden():
[11484]601                self.SubmitDownloadTask(item.domain, item.filename)
[11436]602
[8042]603    def DownloadFiltered(self):
[11484]604        # TODO: submit items in the order shown.
605        for item, tree_item, _ in self.updateItems:
606            if not tree_item.isHidden() and item.state in \
607                    [AVAILABLE, OUTDATED]:
608                self.SubmitDownloadTask(item.domain, item.filename)
[8042]609
610    def SearchUpdate(self, searchString=None):
[11436]611        strings = unicode(self.lineEditFilter.text()).split()
[11484]612        for item, tree_item, _ in self.updateItems:
613            hide = not all(UpdateItem_match(item, string)
614                           for string in strings)
615            tree_item.setHidden(hide)
[11619]616        self.UpdateInfoLabel()
[11693]617        self.toggleButtons()
[11436]618
[11483]619    def SubmitDownloadTask(self, domain, filename):
620        """
621        Submit the (domain, filename) to be downloaded/updated.
622        """
[11693]623        self.cancelButton.setEnabled(True)
624
[11484]625        index = self.updateItemIndex(domain, filename)
626        _, tree_item, opt_widget = self.updateItems[index]
[11483]627
628        if self.accessCode:
629            sf = serverfiles.ServerFiles(access_code=self.accessCode)
630        else:
631            sf = serverfiles.ServerFiles()
632
633        task = DownloadTask(domain, filename, sf)
634
[11484]635        self.executor.submit(task)
[11483]636
637        self.progress.adjustRange(0, 100)
638
639        pb = ItemProgressBar(self.filesView)
640        pb.setRange(0, 100)
641        pb.setTextVisible(False)
642
643        task.advanced.connect(pb.advance)
644        task.advanced.connect(self.progress.advance)
645        task.finished.connect(pb.hide)
646        task.finished.connect(self.onDownloadFinished, Qt.QueuedConnection)
647        task.exception.connect(self.onDownloadError, Qt.QueuedConnection)
648
[11484]649        self.filesView.setItemWidget(tree_item, 2, pb)
[11483]650
651        # Clear the text so it does not show behind the progress bar.
[11484]652        tree_item.setData(2, Qt.DisplayRole, "")
[11483]653        pb.show()
654
[11484]655        # Disable the options widget
656        opt_widget.setEnabled(False)
[11483]657        self._tasks.append(task)
658
659    def EndDownloadTask(self, task):
660        future = task.future()
[11484]661        index = self.updateItemIndex(task.domain, task.filename)
662        item, tree_item, opt_widget = self.updateItems[index]
[11483]663
[11484]664        self.filesView.removeItemWidget(tree_item, 2)
665        opt_widget.setEnabled(True)
[11483]666
667        if future.cancelled():
668            # Restore the previous state
[11484]669            tree_item.setUpdateItem(item)
670            opt_widget.setState(item.state)
[11483]671
672        elif future.exception():
[11484]673            tree_item.setUpdateItem(item)
674            opt_widget.setState(item.state)
675
676            # Show the exception string in the size column.
677            tree_item.setData(2, Qt.DisplayRole,
[11483]678                         QVariant("Error occurred while downloading:" +
679                                  str(future.exception())))
[11484]680
[11483]681        else:
[11484]682            # get the new updated info dict and replace the the old item
683            info = serverfiles.info(item.domain, item.filename)
684            new_item = update_item_from_info(item.domain, item.filename,
685                                             info, info)
686
687            self.updateItems[index] = (new_item, tree_item, opt_widget)
688
689            tree_item.setUpdateItem(new_item)
690            opt_widget.setState(new_item.state)
691
[11483]692            self.UpdateInfoLabel()
[11693]693            self.cancelButton.setEnabled(False)
[11483]694
695    def SubmitRemoveTask(self, domain, filename):
696        serverfiles.remove(domain, filename)
[11484]697        index = self.updateItemIndex(domain, filename)
698        item, tree_item, opt_widget = self.updateItems[index]
[11483]699
[11484]700        if item.info_server:
701            new_item = item._replace(state=AVAILABLE, local=None,
702                                      info_local=None)
703        else:
704            new_item = item._replace(local=None, info_local=None)
705            # Disable the options widget. No more actions can be performed
706            # for the item.
707            opt_widget.setEnabled(False)
708
709        tree_item.setUpdateItem(new_item)
710        opt_widget.setState(new_item.state)
711        self.updateItems[index] = (new_item, tree_item, opt_widget)
[11483]712
713        self.UpdateInfoLabel()
714
715    def Cancel(self):
716        """
717        Cancel all pending update/download tasks (that have not yet started).
718        """
719        for task in self._tasks:
[11484]720            task.future().cancel()
[11483]721
722    def onDeleteWidget(self):
723        self.Cancel()
724        self.executor.shutdown(wait=False)
725        OWBaseWidget.onDeleteWidget(self)
726
727    def onDownloadFinished(self):
728        assert QThread.currentThread() is self.thread()
729        for task in list(self._tasks):
730            future = task.future()
731            if future.done():
732                self.EndDownloadTask(task)
733                self._tasks.remove(task)
734
735        if not self._tasks:
736            # Clear/reset the overall progress
737            self.progress.setRange(0, 0)
738
739    def onDownloadError(self, exc_info):
740        sys.excepthook(*exc_info)
741
[11484]742    def updateItemIndex(self, domain, filename):
743        for i, (item, _, _) in enumerate(self.updateItems):
744            if item.domain == domain and item.filename == filename:
745                return i
746        raise ValueError("%r, %r not in update list" % (domain, filename))
[11483]747
748    def _updateProgress(self, *args):
749        rmin, rmax = self.progress.range()
750        if rmin != rmax:
[11505]751            if not self._haveProgress:
752                self._haveProgress = True
[11483]753                self.progressBarInit()
754
755            self.progressBarSet(self.progress.ratioCompleted() * 100,
756                                processEventsFlags=None)
757        if rmin == rmax:
[11505]758            self._haveProgress = False
[11483]759            self.progressBarFinished()
760
761
762class ProgressState(QObject):
763    valueChanged = Signal(int)
764    rangeChanged = Signal(int, int)
765    textChanged = Signal(str)
766    started = Signal()
767    finished = Signal()
768
769    def __init__(self, parent=None, minimum=0, maximum=0, text="", value=0):
770        QObject.__init__(self, parent)
771
772        self._minimum = minimum
773        self._maximum = max(maximum, minimum)
774        self._text = text
775        self._value = value
776
777    @Slot(int, int)
778    def setRange(self, minimum, maximum):
779        maximum = max(maximum, minimum)
780
781        if self._minimum != minimum or self._maximum != maximum:
782            self._minimum = minimum
783            self._maximum = maximum
784            self.rangeChanged.emit(minimum, maximum)
785
786            # Adjust the value to fit in the range
787            newvalue = min(max(self._value, minimum), maximum)
788            if newvalue != self._value:
789                self.setValue(newvalue)
790
791    def range(self):
792        return self._minimum, self._maximum
793
794    @Slot(int)
795    def setValue(self, value):
796        if self._value != value and value >= self._minimum and \
797                value <= self._maximum:
798            self._value = value
799            self.valueChanged.emit(value)
800
801    def value(self):
802        return self._value
803
804    @Slot(str)
805    def setText(self, text):
806        if self._text != text:
807            self._text = text
808            self.textChanged.emit(text)
809
810    def text(self):
811        return self._text
812
813    @Slot()
814    @Slot(int)
815    def advance(self, value=1):
816        self.setValue(self._value + value)
817
818    def adjustRange(self, dmin, dmax):
819        self.setRange(self._minimum + dmin, self._maximum + dmax)
820
821    def ratioCompleted(self):
822        span = self._maximum - self._minimum
823        if span < 1e-3:
824            return 0.0
825
826        return min(max(float(self._value - self._minimum) / span, 0.0), 1.0)
827
828
829class DownloadTask(Task):
830    advanced = Signal()
831    exception = Signal(tuple)
832
833    def __init__(self, domain, filename, serverfiles, parent=None):
834        Task.__init__(self, parent)
835        self.filename = filename
836        self.domain = domain
837        self.serverfiles = serverfiles
838        self._interrupt = False
839
840    def interrupt(self):
841        """
842        Interrupt the download.
843        """
844        self._interrupt = True
845
846    def _advance(self):
847        self.advanced.emit()
848        if self._interrupt:
849            raise KeyboardInterrupt
850
851    def run(self):
852        try:
853            serverfiles.download(self.domain, self.filename, self.serverfiles,
854                                 callback=self._advance)
855        except Exception:
856            self.exception.emit(sys.exc_info())
857
[11436]858
[8042]859if __name__ == "__main__":
860    app = QApplication(sys.argv)
861    w = OWDatabasesUpdate(wantCloseButton=True)
862    w.show()
863    w.exec_()
Note: See TracBrowser for help on using the repository browser.