1""" Databases update widget """ 2import os 3import sys 4import json 5import threading 6from shutil import copyfile 7from datetime import datetime as d_time 8from functools import partial 9from collections import OrderedDict, namedtuple 10 11from requests.exceptions import Timeout, ConnectionError 12 13from AnyQt.QtCore import Qt, QSize, Signal, QThreadPool 14from AnyQt.QtWidgets import ( 15 QLabel, 16 QDialog, 17 QWidget, 18 QCheckBox, 19 QComboBox, 20 QLineEdit, 21 QFileDialog, 22 QHBoxLayout, 23 QPushButton, 24 QToolButton, 25 QTreeWidget, 26 QVBoxLayout, 27 QApplication, 28 QTreeWidgetItem, 29 QDialogButtonBox, 30 QAbstractItemView, 31 QStyledItemDelegate, 32) 33 34from Orange.widgets import gui 35from Orange.widgets.widget import OWWidget 36 37from orangecontrib.bioinformatics.utils import serverfiles 38from orangecontrib.bioinformatics.geneset import DOMAIN as gene_sets_domain 39from orangecontrib.bioinformatics.geneset import filename 40from orangecontrib.bioinformatics.go.config import DOMAIN as gene_ontology_domain 41from orangecontrib.bioinformatics.go.config import FILENAME_ANNOTATION 42from orangecontrib.bioinformatics.ncbi.taxonomy import common_taxids, common_taxid_to_name, species_name_to_taxid 43from orangecontrib.bioinformatics.widgets.utils.gui import TokenListCompleter 44from orangecontrib.bioinformatics.widgets.utils.concurrent import Worker 45 46# File states 47AVAILABLE, CURRENT, OUTDATED, DEPRECATED, USER_FILE = range(5) 48# File sources 49SOURCE_SERVER = 'server_file' # files on the serverfiles-bio repository 50SOURCE_USER = 'user_file' # user defined files 51INFO_FILE_SCHEMA = { 52 'domain': None, 53 'filename': None, 54 'source': None, 55 'title': None, 56 'tags': [], 57 'size': None, 58 'datetime': None 59 # used only if files are compressed 60 # 'uncompressed': None, 61 # 'compression': None, 62} 63 64 65def file_size_bytes(file_path): 66 """ returns file size in bytes """ 67 return os.stat(file_path).st_size 68 69 70def create_info_file(file_path, **kwargs): 71 info_dict = OrderedDict(INFO_FILE_SCHEMA) 72 73 info_dict.update(**kwargs) 74 info_dict['datetime'] = '{0:%Y-%m-%d %H:%M:%S.%f}'.format(d_time.today()) 75 info_dict['size'] = file_size_bytes(file_path) 76 77 with open(file_path + '.info', 'wt') as f: 78 json.dump(info_dict, f) 79 80 81def create_folder(path): 82 try: 83 os.makedirs(path) 84 except OSError: 85 if os.path.exists(path): 86 pass 87 else: 88 # There was an error on creation, so make sure we know about it 89 raise 90 91 92class UpdateOptionsItemDelegate(QStyledItemDelegate): 93 """ An item delegate for the updates tree widget. 94 95 note: 96 Must be a child of a QTreeWidget. 97 98 """ 99 100 def sizeHint(self, option, index): 101 size = QStyledItemDelegate.sizeHint(self, option, index) 102 parent = self.parent() 103 item = parent.itemFromIndex(index) 104 widget = parent.itemWidget(item, 0) 105 if widget: 106 size = QSize(size.width(), widget.sizeHint().height() / 2) 107 return size 108 109 110file_state = namedtuple('file_state', ['info_local', 'info_server', 'state']) 111 112header_labels = ['', 'Title', 'Update', 'Updated', 'Size', 'Source'] 113header_index = namedtuple('header_index', ['Download'] + header_labels[1:]) 114header = header_index(*[index for index, _ in enumerate(header_labels)]) 115 116 117def UpdateItem_match(item, string): 118 """ 119 Return `True` if the `UpdateItem` item contains a string in tags 120 or in the title. 121 122 """ 123 string = string.lower() 124 return any(string.lower() in tag.lower() for tag in item.tags + [item.title]) 125 126 127def evaluate_all_info(local, server): 128 """ Return FileState 129 130 Args: 131 local: info files from LocalFiles 132 server: info files from ServerFiles 133 134 """ 135 files = set(local.keys()).union(server.keys()) 136 137 for domain, file_name in sorted(files): 138 yield FileState(domain, file_name, server.get((domain, file_name), None), local.get((domain, file_name), None)) 139 140 141def evaluate_files_state(progress_callback): 142 progress_callback.emit() 143 files = [] 144 145 # fetch remote info 146 try: 147 server_info = serverfiles.ServerFiles().allinfo() 148 except (Timeout, ConnectionError) as e: 149 raise e 150 progress_callback.emit() 151 152 # fetch local info 153 local_info = serverfiles.allinfo() 154 155 all_info = set(local_info.keys()).union(server_info.keys()) 156 157 for domain, file_name in sorted(all_info): 158 files.append( 159 FileState( 160 domain, 161 file_name, 162 server_info.get((domain, file_name), None), 163 local_info.get((domain, file_name), None), 164 ) 165 ) 166 progress_callback.emit() 167 return files 168 169 170def download_server_file(fs, index, progress_callback): 171 try: 172 serverfiles.download(fs.domain, fs.filename, callback=progress_callback.emit) 173 except Exception: 174 # send FileState and index with Exception 175 raise ValueError(fs, index) 176 177 return fs, index 178 179 180class OWDatabasesUpdate(OWWidget): 181 182 name = "Databases Update" 183 description = "Update local systems biology databases." 184 icon = "../widgets/icons/OWDatabasesUpdate.svg" 185 priority = 1 186 187 inputs = [] 188 outputs = [] 189 190 want_main_area = False 191 192 def __init__(self, parent=None, signalManager=None, name="Databases update"): 193 OWWidget.__init__(self, parent, signalManager, name, wantMainArea=False) 194 195 self.searchString = "" 196 197 fbox = gui.widgetBox(self.controlArea, "Filter") 198 self.completer = TokenListCompleter(self, caseSensitivity=Qt.CaseInsensitive) 199 self.lineEditFilter = QLineEdit(textChanged=self.search_update) 200 self.lineEditFilter.setCompleter(self.completer) 201 202 fbox.layout().addWidget(self.lineEditFilter) 203 204 box = gui.widgetBox(self.controlArea, "Files") 205 self.filesView = QTreeWidget(self) 206 self.filesView.setHeaderLabels(header_labels) 207 self.filesView.setRootIsDecorated(False) 208 self.filesView.setUniformRowHeights(True) 209 self.filesView.setSelectionMode(QAbstractItemView.NoSelection) 210 self.filesView.setSortingEnabled(True) 211 self.filesView.sortItems(header.Title, Qt.AscendingOrder) 212 self.filesView.setItemDelegateForColumn(0, UpdateOptionsItemDelegate(self.filesView)) 213 214 self.filesView.model().layoutChanged.connect(self.search_update) 215 216 box.layout().addWidget(self.filesView) 217 218 layout = QHBoxLayout() 219 gui.widgetBox(self.controlArea, margin=0, orientation=layout) 220 221 self.updateButton = gui.button( 222 box, self, "Update all", callback=self.update_all, tooltip="Update all updatable files" 223 ) 224 225 self.downloadButton = gui.button( 226 box, self, "Download all", callback=self.download_filtered, tooltip="Download all filtered files shown" 227 ) 228 229 self.cancelButton = gui.button( 230 box, self, "Cancel", callback=self.cancel_active_threads, tooltip="Cancel scheduled downloads/updates." 231 ) 232 233 self.addButton = gui.button( 234 box, self, "Add ...", callback=self.__handle_dialog, tooltip="Add files for personal use." 235 ) 236 237 layout.addWidget(self.updateButton) 238 layout.addWidget(self.downloadButton) 239 layout.addWidget(self.cancelButton) 240 layout.addStretch() 241 layout.addWidget(self.addButton) 242 243 # Enable retryButton once connection is established 244 # self.retryButton = gui.button( 245 # box, self, "Reconnect", callback=self.initialize_files_view 246 # ) 247 # self.retryButton.hide() 248 249 self.resize(800, 600) 250 251 self.update_items = [] 252 self._dialog = None 253 self.progress_bar = None 254 255 # threads 256 self.threadpool = QThreadPool(self) 257 # self.threadpool.setMaxThreadCount(1) 258 self.workers = [] 259 260 self.initialize_files_view() 261 262 def __handle_dialog(self): 263 if not self._dialog: 264 self._dialog = FileUploadHelper(self) 265 self._dialog.show() 266 267 def __progress_advance(self): 268 # GUI should be updated in main thread. That's why we are calling advance method here 269 if self.progress_bar: 270 self.progress_bar.advance() 271 272 def handle_worker_exception(self, ex): 273 self.progress_bar.finish() 274 self.setStatusMessage('') 275 276 if isinstance(ex, ConnectionError): 277 # TODO: set warning messages 278 pass 279 280 print(ex) 281 282 def initialize_files_view(self): 283 # self.retryButton.hide() 284 285 # clear view 286 self.filesView.clear() 287 # init progress bar 288 self.progress_bar = gui.ProgressBar(self, iterations=3) 289 # status message 290 self.setStatusMessage('initializing') 291 292 worker = Worker(evaluate_files_state, progress_callback=True) 293 worker.signals.progress.connect(self.__progress_advance) 294 worker.signals.result.connect(self.set_files_list) 295 worker.signals.error.connect(self.handle_worker_exception) 296 297 # move download process to worker thread 298 self.threadpool.start(worker) 299 self.setEnabled(False) 300 301 def __create_action_button(self, fs, retry=None): 302 if not fs.state not in [OUTDATED, USER_FILE] or not retry: 303 self.filesView.setItemWidget(fs.tree_item, header.Update, None) 304 305 button = QToolButton(None) 306 if not retry: 307 if fs.state == OUTDATED: 308 button.setText('Update') 309 button.clicked.connect(partial(self.submit_download_task, fs.domain, fs.filename, True)) 310 elif fs.state == USER_FILE: 311 if not fs.info_server: 312 button.setText('Remove') 313 button.clicked.connect(partial(self.submit_remove_task, fs.domain, fs.filename)) 314 else: 315 button.setText('Use server version') 316 button.clicked.connect(partial(self.submit_download_task, fs.domain, fs.filename, True)) 317 else: 318 button.setText('Retry') 319 button.clicked.connect(partial(self.submit_download_task, fs.domain, fs.filename, True)) 320 321 button.setMaximumWidth(120) 322 button.setMaximumHeight(20) 323 button.setMinimumHeight(20) 324 325 if sys.platform == "darwin": 326 button.setAttribute(Qt.WA_MacSmallSize) 327 328 self.filesView.setItemWidget(fs.tree_item, header.Update, button) 329 330 def set_files_list(self, result): 331 """ Set the files to show. 332 """ 333 assert threading.current_thread() == threading.main_thread() 334 self.progress_bar.finish() 335 self.setStatusMessage('') 336 self.setEnabled(True) 337 338 self.update_items = result 339 all_tags = set() 340 341 for fs in self.update_items: 342 fs.tree_item = FileStateItem(fs) 343 fs.download_option = DownloadOption(state=fs.state) 344 345 fs.download_option.download_clicked.connect(partial(self.submit_download_task, fs.domain, fs.filename)) 346 fs.download_option.remove_clicked.connect(partial(self.submit_remove_task, fs.domain, fs.filename)) 347 348 # add widget items to the QTreeWidget 349 self.filesView.addTopLevelItems([fs.tree_item for fs in self.update_items]) 350 351 # add action widgets to tree items 352 for fs in self.update_items: 353 self.filesView.setItemWidget(fs.tree_item, header.Download, fs.download_option) 354 if fs.state in [USER_FILE, OUTDATED]: 355 self.__create_action_button(fs) 356 357 all_tags.update(fs.tags) 358 359 self.filesView.setColumnWidth(header.Download, self.filesView.sizeHintForColumn(header.Download)) 360 361 for column in range(1, len(header_labels)): 362 self.filesView.resizeColumnToContents(column) 363 364 hints = [hint for hint in sorted(all_tags) if not hint.startswith("#")] 365 self.completer.setTokenList(hints) 366 self.search_update() 367 self.toggle_action_buttons() 368 self.cancelButton.setEnabled(False) 369 370 def toggle_action_buttons(self): 371 selected_items = [fs for fs in self.update_items if not fs.tree_item.isHidden()] 372 373 def button_check(sel_items, state, button): 374 for item in sel_items: 375 if item.state != state: 376 button.setEnabled(False) 377 else: 378 button.setEnabled(True) 379 break 380 381 button_check(selected_items, OUTDATED, self.updateButton) 382 button_check(selected_items, AVAILABLE, self.downloadButton) 383 384 def search_update(self, searchString=None): 385 strings = str(self.lineEditFilter.text()).split() 386 for fs in self.update_items: 387 hide = not all(UpdateItem_match(fs, string) for string in strings) 388 fs.tree_item.setHidden(hide) 389 self.toggle_action_buttons() 390 391 def update_all(self): 392 for fs in self.update_items: 393 if fs.state == OUTDATED and not fs.tree_item.isHidden(): 394 self.submit_download_task(fs.domain, fs.filename) 395 396 def download_filtered(self): 397 for fs in self.update_items: 398 if not fs.tree_item.isHidden() and fs.state in [AVAILABLE, OUTDATED]: 399 self.submit_download_task(fs.domain, fs.filename, start=False) 400 401 self.run_download_tasks() 402 403 def submit_download_task(self, domain, filename, start=True): 404 """ Submit the (domain, filename) to be downloaded/updated. 405 """ 406 # get selected tree item 407 index = self.tree_item_index(domain, filename) 408 fs = self.update_items[index] 409 410 worker = Worker(download_server_file, fs, index, progress_callback=True) 411 worker.signals.progress.connect(self.__progress_advance) 412 worker.signals.result.connect(self.on_download_finished) 413 worker.signals.error.connect(self.on_download_exception) 414 415 self.workers.append(worker) 416 417 if start: 418 self.run_download_tasks() 419 420 def run_download_tasks(self): 421 self.cancelButton.setEnabled(True) 422 # init progress bar 423 424 self.progress_bar = gui.ProgressBar(self, iterations=len(self.workers) * 100) 425 426 # status message 427 self.setStatusMessage('downloading') 428 429 # move workers to threadpool 430 [self.threadpool.start(worker) for worker in self.workers] 431 self.filesView.setDisabled(True) 432 # reset list of workers 433 self.workers = [] 434 435 def on_download_exception(self, ex): 436 assert threading.current_thread() == threading.main_thread() 437 self.progress_bar.finish() 438 self.setStatusMessage('') 439 print(ex) 440 if isinstance(ex, ValueError): 441 fs, index = ex.args 442 443 # restore state and retry 444 fs.refresh_state() 445 fs.tree_item.update_data(fs) 446 fs.download_option.state = fs.state 447 self.__create_action_button(fs, retry=True) 448 449 def on_download_finished(self, result): 450 assert threading.current_thread() == threading.main_thread() 451 452 # We check if all workers have completed. If not, continue 453 if self.progress_bar.count == 100 or self.threadpool.activeThreadCount() == 0: 454 self.filesView.setDisabled(False) 455 self.progress_bar.finish() 456 self.setStatusMessage('') 457 458 fs, index = result 459 # re-evaluate File State 460 info = serverfiles.info(fs.domain, fs.filename) 461 fs.refresh_state(info_local=info, info_server=info) 462 # reinitialize treeWidgetItem 463 fs.tree_item.update_data(fs) 464 # reinitialize OptionWidget 465 fs.download_option.state = fs.state 466 self.filesView.setItemWidget(fs.tree_item, header.Update, None) 467 468 self.toggle_action_buttons() 469 for column in range(1, len(header_labels)): 470 self.filesView.resizeColumnToContents(column) 471 472 def submit_remove_task(self, domain, filename): 473 serverfiles.LOCALFILES.remove(domain, filename) 474 475 index = self.tree_item_index(domain, filename) 476 fs = self.update_items[index] 477 478 if fs.state == USER_FILE: 479 self.filesView.takeTopLevelItem(self.filesView.indexOfTopLevelItem(fs.tree_item)) 480 self.update_items.remove(fs) 481 # self.filesView.removeItemWidget(index) 482 else: 483 # refresh item state 484 fs.info_local = None 485 fs.refresh_state() 486 # reinitialize treeWidgetItem 487 fs.tree_item.update_data(fs) 488 # reinitialize OptionWidget 489 fs.download_option.state = fs.state 490 491 self.toggle_action_buttons() 492 493 def cancel_active_threads(self): 494 """ Cancel all pending update/download tasks (that have not yet started). 495 """ 496 if self.threadpool: 497 self.threadpool.clear() 498 499 def tree_item_index(self, domain, filename): 500 for i, fs in enumerate(self.update_items): 501 if fs.domain == domain and fs.filename == filename: 502 return i 503 raise ValueError("%r, %r not in update list" % (domain, filename)) 504 505 def onDeleteWidget(self): 506 self.cancel_active_threads() 507 OWWidget.onDeleteWidget(self) 508 509 510class DownloadOption(QWidget): 511 """ A Widget with download/update/remove options. 512 """ 513 514 download_clicked = Signal() 515 remove_clicked = Signal() 516 517 def __init__(self, state=AVAILABLE, parent=None): 518 QWidget.__init__(self, parent) 519 layout = QHBoxLayout() 520 layout.setSpacing(1) 521 layout.setContentsMargins(1, 1, 1, 1) 522 523 self.checkButton = QCheckBox() 524 525 layout.addWidget(self.checkButton) 526 self.setLayout(layout) 527 528 self.setMinimumHeight(20) 529 self.setMaximumHeight(20) 530 531 self._state = state 532 self._update() 533 534 @property 535 def state(self): 536 return self._state 537 538 @state.setter 539 def state(self, state): 540 self._state = state 541 self._update() 542 543 def _update(self): 544 self.checkButton.setDisabled(False) 545 546 if self.state == AVAILABLE: 547 self.checkButton.setChecked(False) 548 elif self.state == CURRENT: 549 self.checkButton.setChecked(True) 550 elif self.state == OUTDATED: 551 self.checkButton.setChecked(True) 552 elif self.state == DEPRECATED: 553 self.checkButton.setChecked(True) 554 elif self.state == USER_FILE: 555 self.checkButton.setChecked(False) 556 self.checkButton.setDisabled(True) 557 else: 558 raise ValueError("Invalid state %r" % self.state) 559 560 try: 561 self.checkButton.clicked.disconnect() # Remove old signals if they exist 562 except Exception: 563 pass 564 565 if not self.checkButton.isChecked(): # Switch signals if the file is present or not 566 self.checkButton.clicked.connect(self.download_clicked) 567 else: 568 self.checkButton.clicked.connect(self.remove_clicked) 569 570 571class FileState: 572 def __init__(self, domain, file_name, info_server, info_local): 573 self.domain = domain 574 self.filename = file_name 575 576 self.info_server = self.parse_info_file(info_server) 577 self.info_local = self.parse_info_file(info_local) 578 579 # self.source = None 580 self.state = self.__item_state() 581 582 self.tree_item = None 583 self.download_option = None 584 585 def refresh_state(self, info_server=None, info_local=None): 586 if info_local: 587 self.info_local = self.parse_info_file(info_local) 588 if info_server: 589 self.info_server = self.parse_info_file(info_server) 590 591 self.state = self.__item_state() 592 593 @property 594 def tags(self): 595 if self.state in [AVAILABLE]: 596 return self.info_server.tags 597 elif self.state in [OUTDATED, USER_FILE, CURRENT]: 598 return self.info_local.tags 599 else: 600 return None 601 602 @property 603 def title(self): 604 if self.state in [AVAILABLE]: 605 return self.info_server.title 606 elif self.state in [OUTDATED, USER_FILE, CURRENT]: 607 return self.info_local.title 608 else: 609 return None 610 611 @property 612 def size(self): 613 if self.state in [AVAILABLE]: 614 return self.info_server.size 615 elif self.state in [OUTDATED, USER_FILE, CURRENT]: 616 return self.info_local.size 617 else: 618 return None 619 620 @property 621 def datetime(self): 622 if self.state in [USER_FILE, CURRENT, OUTDATED]: 623 return self.info_local.datetime 624 else: 625 return self.info_server.datetime 626 627 @property 628 def source(self): 629 if self.state is USER_FILE: 630 return self.info_local.source.capitalize().replace('_', ' ') 631 else: 632 return self.info_server.source.capitalize().replace('_', ' ') 633 634 def __item_state(self): 635 """ Return item state 636 637 Note: 638 available -> available for download 639 current -> latest version downloaded 640 outdated -> needs update (newer version on serverfiles-bio repository) 641 deprecated -> removed from serverfiles-bio repository 642 user_file -> not in serverfiles-bio repository (user defined) 643 644 """ 645 646 if not self.info_server and self.info_local: 647 # we check source of the file 648 if self.info_local.source == SOURCE_USER: 649 # this is user defined file 650 return USER_FILE 651 652 elif self.info_local.source == SOURCE_SERVER: 653 # there is no record of this file on the server 654 return DEPRECATED 655 656 if not self.info_local and self.info_server: 657 return AVAILABLE 658 659 if self.info_server and self.info_local: 660 if not self.info_local.source == SOURCE_USER: 661 662 if self.info_local.datetime < self.info_server.datetime: 663 return OUTDATED 664 else: 665 return CURRENT 666 667 else: 668 return USER_FILE 669 670 @staticmethod 671 def parse_info_file(info): 672 """ Parse .info file from JSON like format to namedtuple 673 """ 674 if info is not None: 675 if not isinstance(info['datetime'], d_time): 676 info['datetime'] = d_time.strptime(info['datetime'], "%Y-%m-%d %H:%M:%S.%f") 677 return namedtuple('file_info', info.keys())(**info) 678 679 680class FileStateItem(QTreeWidgetItem): 681 682 STATE_STRINGS = { 683 0: 'not downloaded', 684 1: 'downloaded, current', 685 2: 'downloaded, needs update', 686 3: 'obsolete', 687 4: 'custom file', 688 } 689 690 #: A role for the state item data. 691 StateRole = next(gui.OrangeUserRole) 692 693 # QTreeWidgetItem stores the DisplayRole and EditRole as the same role, 694 # so we can't use EditRole to store the actual item data, instead we use 695 # custom role. 696 697 #: A custom edit role for the item's data 698 #: (QTreeWidget treats Qt.EditRole as a alias for Qt.DisplayRole) 699 EditRole2 = next(gui.OrangeUserRole) 700 701 def __init__(self, fs): 702 """ A QTreeWidgetItem for displaying a FileState. 703 """ 704 QTreeWidgetItem.__init__(self, type=QTreeWidgetItem.UserType) 705 self.update_data(fs) 706 self._update_tool_tip(fs) 707 708 def update_data(self, fs): 709 self.setData(header.Download, FileStateItem.StateRole, fs.state) 710 711 self.setData(header.Source, Qt.DisplayRole, fs.source) 712 self.setData(header.Source, self.EditRole2, fs.source) 713 714 self.setData(header.Title, Qt.DisplayRole, fs.title) 715 self.setData(header.Title, self.EditRole2, fs.title) 716 717 if not fs.state == AVAILABLE: 718 self.setData(header.Updated, Qt.DisplayRole, fs.datetime.date().isoformat()) 719 self.setData(header.Updated, self.EditRole2, fs.datetime) 720 else: 721 self.setData(header.Updated, Qt.DisplayRole, '') 722 self.setData(header.Updated, self.EditRole2, '') 723 724 self.setData(header.Size, Qt.DisplayRole, serverfiles.sizeformat(fs.size)) 725 self.setData(header.Size, self.EditRole2, fs.size) 726 727 def _update_tool_tip(self, fs): 728 state_str = self.STATE_STRINGS[fs.state] 729 if fs == DEPRECATED: 730 diff_date = fs.info_server.datetime - fs.info_local.datetime 731 else: 732 diff_date = None 733 734 tooltip = "State: {}\nTags: {}".format(state_str, ', '.join(tag for tag in fs.tags if not tag.startswith("#"))) 735 736 if fs.state in [CURRENT, OUTDATED, DEPRECATED]: 737 tooltip += "\nFile: {}".format(serverfiles.localpath(fs.domain, fs.filename)) 738 739 if fs.state == OUTDATED and diff_date: 740 tooltip += "\nServer version: {}\nStatus: old {} days".format(fs.datetime, diff_date.days) 741 else: 742 tooltip += "\nServer version: {}".format(fs.datetime) 743 744 for i in range(1, len(header_labels) - 1): 745 self.setToolTip(i, tooltip) 746 747 def __lt__(self, other): 748 widget = self.treeWidget() 749 column = widget.sortColumn() 750 if column == 0: 751 role = FileStateItem.StateRole 752 else: 753 role = self.EditRole2 754 755 left = self.data(column, role) 756 right = other.data(column, role) 757 try: 758 return left < right 759 except TypeError: 760 pass 761 # order lexically by str representation, but ensure `None` 762 # always orders on one side 763 left = (0, "") if left is None else (1, str(left)) 764 right = (0, "") if right is None else (1, str(right)) 765 return left < right 766 767 768class FileUploadHelper(QDialog): 769 770 # settings 771 kegg_domain = 'KEGG' 772 773 supported_domains = OrderedDict({'Gene Ontology': gene_ontology_domain, 'Gene Sets': gene_sets_domain}) 774 775 supported_organisms = [common_taxid_to_name(tax_id) for tax_id in common_taxids()] 776 777 hierarchies = { 778 'GO - Biological Process': ('GO', 'biological_process'), 779 'GO - Molecular Function': ('GO', 'molecular_function'), 780 'GO - Cellular Component': ('GO', 'cellular_component'), 781 'KEGG - Pathways': ('KEGG', 'pathways'), 782 'KEGG - Orthologs': ('KEGG', 'orthologs'), 783 } 784 785 def __init__(self, parent=None): 786 super(FileUploadHelper, self).__init__( 787 parent, 788 Qt.Window 789 | Qt.WindowTitleHint 790 | Qt.CustomizeWindowHint 791 | Qt.WindowCloseButtonHint 792 | Qt.WindowMaximizeButtonHint, 793 ) 794 self.setAttribute(Qt.WA_DeleteOnClose) 795 self.setWindowTitle('Add new file') 796 797 self.info_state = INFO_FILE_SCHEMA 798 self.layout = QVBoxLayout(self) 799 800 # domain selection combobox 801 self.domain_selection = QComboBox() 802 self.domain_selection.addItems(self.supported_domains.keys()) 803 self.domain_selection.currentIndexChanged.connect(self.__on_domain_selection) 804 self.__create_selection_row('Domain: ', self.domain_selection) 805 806 # domain selection combobox 807 self.hierarchy_selection = QComboBox() 808 self.hierarchy_selection.addItems(self.hierarchies.keys()) 809 self.layout.addWidget(self.hierarchy_selection, alignment=Qt.AlignVCenter) 810 self.__on_domain_selection() 811 812 # select organism 813 self.organism_selection = QComboBox() 814 self.organism_selection.addItems(self.supported_organisms) 815 self.__create_selection_row('Organism: ', self.organism_selection) 816 817 # title 818 self.line_edit_title = QLineEdit() 819 self.__create_selection_row('Title: ', self.line_edit_title) 820 821 # tags 822 self.line_edit_tags = QLineEdit() 823 self.__create_selection_row('Tags (comma-separated): ', self.line_edit_tags) 824 825 # file selector 826 self.file_info = QLabel() 827 self.file_select_btn = QPushButton('Select File', self) 828 self.file_select_btn.clicked.connect(self.__handle_file_selector) 829 self.__create_selection_row(' ', self.file_select_btn) 830 831 # add file info section 832 self.layout.addWidget(self.file_info, alignment=Qt.AlignCenter) 833 834 self.layout.addStretch(1) 835 836 # Ok and Cancel buttons 837 self.buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel, Qt.Horizontal, self) 838 self.layout.addWidget(self.buttons, alignment=Qt.AlignJustify) 839 840 self.buttons.accepted.connect(self.__accept) 841 self.buttons.rejected.connect(self.__close) 842 843 # path to a selected file 844 self.file_path = None 845 846 def __on_domain_selection(self): 847 selected = self.__get_selected_domain() == gene_sets_domain 848 self.hierarchy_selection.setVisible(selected) 849 850 def __get_selected_domain(self): 851 domain_label = list(self.supported_domains.keys())[self.domain_selection.currentIndex()] 852 return self.supported_domains[domain_label] 853 854 def __get_selected_hier(self): 855 hier_label = list(self.hierarchies.keys())[self.hierarchy_selection.currentIndex()] 856 return self.hierarchies[hier_label] 857 858 def __create_selection_row(self, label, widget): 859 self.layout.addWidget(QLabel(label), alignment=Qt.AlignLeft) 860 self.layout.addWidget(widget, alignment=Qt.AlignVCenter) 861 862 def __accept(self): 863 if self.file_path: 864 self.info_state = self.__parse_selection() 865 self.__move_to_serverfiles_folder(self.file_path) 866 867 self.parent().initialize_files_view() 868 self.close() 869 870 def __close(self): 871 self.close() 872 873 def closeEvent(self, event): 874 # clean-up 875 self.parent()._dialog = None 876 877 def __filename(self, domain, organism): 878 """ Create filename based od domain name and organism. 879 """ 880 881 if domain in self.supported_domains.values() and domain == gene_ontology_domain and organism: 882 return FILENAME_ANNOTATION.format(organism) 883 884 elif domain in self.supported_domains.values() and domain == gene_sets_domain and organism: 885 return filename((self.__get_selected_hier()), organism) 886 887 def __parse_selection(self): 888 try: 889 domain = self.__get_selected_domain() 890 organism = species_name_to_taxid(self.supported_organisms[self.organism_selection.currentIndex()]) 891 except KeyError as e: 892 raise e 893 894 return { 895 'domain': domain, 896 'organism': organism, 897 'filename': self.__filename(domain, organism), 898 'title': self.line_edit_title.text(), 899 'tags': self.line_edit_tags.text().split(','), 900 'source': SOURCE_USER, 901 } 902 903 def __move_to_serverfiles_folder(self, selected_file_path): 904 domain_path = serverfiles.localpath(self.info_state['domain']) 905 file_path = os.path.join(domain_path, self.info_state['filename']) 906 create_folder(domain_path) 907 908 try: 909 copyfile(selected_file_path, file_path) 910 except IOError as e: 911 # TODO: handle error properly 912 raise e 913 914 # if copy successful create .info file 915 create_info_file(file_path, **self.info_state) 916 917 def __handle_file_selector(self): 918 self.file_path = QFileDialog.getOpenFileName(self, 'Open File')[0] 919 self.file_info.setText('Selected File: {}'.format(os.path.basename(self.file_path))) 920 921 922if __name__ == "__main__": 923 924 def main_test(): 925 app = QApplication(sys.argv) 926 w = OWDatabasesUpdate() 927 w.show() 928 w.raise_() 929 return app.exec_() 930 931 sys.exit(main_test()) 932