def test_runJavaScript(self): w = WebviewWidget() w.runJavaScript('2;') retvals = [] w.runJavaScript('3;', lambda retval: retvals.append(retval)) wait(until=lambda: retvals) self.assertEqual(retvals[0], 3)
def test_escape_hides(self): window = QDialog() w = WebviewWidget(window) window.show() w.setFocus(Qt.OtherFocusReason) self.assertFalse(window.isHidden()) QTest.keyClick(w, Qt.Key_Escape) self.assertTrue(window.isHidden())
def _setup_ui_(self): self.table_model = ReportItemModel(0, len(Column.__members__)) self.table = ReportTable(self.controlArea) self.table.setModel(self.table_model) self.table.setShowGrid(False) self.table.setSelectionBehavior(QTableView.SelectRows) self.table.setSelectionMode(QTableView.SingleSelection) self.table.setWordWrap(False) self.table.setMouseTracking(True) self.table.verticalHeader().setSectionResizeMode(QHeaderView.Fixed) self.table.verticalHeader().setDefaultSectionSize(20) self.table.verticalHeader().setVisible(False) self.table.horizontalHeader().setVisible(False) self.table.setFixedWidth(250) self.table.setColumnWidth(Column.item, 200) self.table.setColumnWidth(Column.remove, 23) self.table.setColumnWidth(Column.scheme, 25) self.table.clicked.connect(self._table_clicked) self.table.selectionModel().selectionChanged.connect( self._table_selection_changed) self.controlArea.layout().addWidget(self.table) self.last_scheme = None self.scheme_button = gui.button( self.controlArea, self, "Back to Last Scheme", callback=self._show_last_scheme, ) box = gui.hBox(self.controlArea) box.setContentsMargins(-6, 0, -6, 0) self.save_button = gui.button(box, self, "Save", callback=self.save_report) self.print_button = gui.button(box, self, "Print", callback=self._print_report) class PyBridge(QObject): @pyqtSlot(str) def _select_item(myself, item_id): item = self.table_model.get_item_by_id(item_id) self.table.selectRow( self.table_model.indexFromItem(item).row()) self._change_selected_item(item) @pyqtSlot(str, str) def _add_comment(myself, item_id, value): item = self.table_model.get_item_by_id(item_id) item.comment = value self.report_changed = True self.report_view = WebviewWidget(self.mainArea, bridge=PyBridge(self)) self.mainArea.layout().addWidget(self.report_view)
def test_escape_hides(self): # NOTE: This test doesn't work as it is supposed to. window = QDialog() w = WebviewWidget(window) window.show() w.setFocus(Qt.OtherFocusReason) self.assertFalse(window.isHidden()) # This event is sent to the wrong widget. Should be sent to the # inner HTML view as focused, but no amount of clicking/ focusing # helped, neither did invoking JS handler directly. I'll live with it. QTest.keyClick(w, Qt.Key_Escape) self.assertTrue(window.isHidden())
def test_base(self): w = WebviewWidget() w.evalJS('document.write("foo");') SVG = '<svg xmlns:dc="...">asd</svg>' w.onloadJS('''document.write('{}');'''.format(SVG)) w.setUrl(SOME_URL) svg = self.process_events(lambda: w.svg()) self.assertEqual(svg, SVG) self.assertEqual( w.html(), '<html><head></head><body>foo<svg xmlns:dc="...">asd</svg></body></html>' )
def test_base(self): w = WebviewWidget() w.evalJS('document.write("foo");') SVG = '<svg xmlns:dc="...">asd</svg>' w.onloadJS('''document.write('{}');'''.format(SVG)) w.setUrl(SOME_URL) svg = self.process_events(lambda: w.svg()) self.assertEqual(svg, SVG) self.assertEqual( w.html(), '<html><head></head><body>foo<svg xmlns:dc="...">asd</svg></body></html>')
def test_base(self): w = WebviewWidget() w.evalJS('document.write("foo");') SVG = '<svg xmlns:dc="...">asd</svg>' w.onloadJS('''document.write('{}');'''.format(SVG)) w.setUrl(SOME_URL) svg = None while svg is None: try: svg = w.svg() break except ValueError: qApp.processEvents() self.assertEqual(svg, SVG) self.assertEqual( w.html(), '<html><head></head><body>foo<svg xmlns:dc="...">asd</svg></body></html>')
def test_base(self): w = WebviewWidget() w.evalJS('document.write("foo");') SVG = '<svg xmlns:dc="...">asd</svg>' w.onloadJS('''document.write('{}');'''.format(SVG)) w.setUrl(SOME_URL) svg = self.process_events(lambda: w.svg()) self.assertEqual(svg, SVG) self.process_events(until=lambda: 'foo' in w.html()) html = '<svg xmlns:dc="...">asd</svg>' self.assertEqual( w.html(), '<html><head></head><body>{}</body></html>'.format( # WebKit evaluates first document.write first, whereas # WebEngine evaluates onloadJS first 'foo' + html if HAVE_WEBKIT else html + 'foo' ))
def _setup_ui_(self): self.table_model = ReportItemModel(0, len(Column.__members__)) self.table = ReportTable(self.controlArea) self.table.setModel(self.table_model) self.table.setShowGrid(False) self.table.setSelectionBehavior(QTableView.SelectRows) self.table.setSelectionMode(QTableView.SingleSelection) self.table.setWordWrap(False) self.table.setMouseTracking(True) self.table.verticalHeader().setSectionResizeMode(QHeaderView.Fixed) self.table.verticalHeader().setDefaultSectionSize(20) self.table.verticalHeader().setVisible(False) self.table.horizontalHeader().setVisible(False) self.table.setFixedWidth(250) self.table.setColumnWidth(Column.item, 200) self.table.setColumnWidth(Column.remove, 23) self.table.setColumnWidth(Column.scheme, 25) self.table.clicked.connect(self._table_clicked) self.table.selectionModel().selectionChanged.connect( self._table_selection_changed) self.controlArea.layout().addWidget(self.table) self.last_scheme = None self.scheme_button = gui.button( self.controlArea, self, "Back to Last Scheme", callback=self._show_last_scheme ) box = gui.hBox(self.controlArea) box.setContentsMargins(-6, 0, -6, 0) self.save_button = gui.button( box, self, "Save", callback=self.save_report ) self.print_button = gui.button( box, self, "Print", callback=self._print_report ) class PyBridge(QObject): @pyqtSlot(str) def _select_item(myself, item_id): item = self.table_model.get_item_by_id(item_id) self.table.selectRow(self.table_model.indexFromItem(item).row()) self._change_selected_item(item) @pyqtSlot(str, str) def _add_comment(myself, item_id, value): item = self.table_model.get_item_by_id(item_id) item.comment = value self.report_changed = True self.report_view = WebviewWidget(self.mainArea, bridge=PyBridge(self)) self.mainArea.layout().addWidget(self.report_view)
def test_exposeObject(self): test = self OBJ = dict(a=[1, 2], b='c') done = False class Bridge(QObject): @pyqtSlot('QVariantMap') def check_object(self, obj): nonlocal test, done, OBJ done = True test.assertEqual(obj, OBJ) w = WebviewWidget(bridge=Bridge()) w.setUrl(SOME_URL) w.exposeObject('obj', OBJ) w.evalJS('''pybridge.check_object(window.obj);''') self.process_events(lambda: done) self.assertRaises(ValueError, w.exposeObject, 'obj', QDialog())
class OWReport(OWWidget): name = "Report" save_dir = Setting("") open_dir = Setting("") def __init__(self): super().__init__() self._setup_ui_() self.report_changed = False index_file = pkg_resources.resource_filename(__name__, "index.html") with open(index_file, "r") as f: self.report_html_template = f.read() def _setup_ui_(self): self.table_model = ReportItemModel(0, len(Column.__members__)) self.table = ReportTable(self.controlArea) self.table.setModel(self.table_model) self.table.setShowGrid(False) self.table.setSelectionBehavior(QTableView.SelectRows) self.table.setSelectionMode(QTableView.SingleSelection) self.table.setWordWrap(False) self.table.setMouseTracking(True) self.table.verticalHeader().setSectionResizeMode(QHeaderView.Fixed) self.table.verticalHeader().setDefaultSectionSize(20) self.table.verticalHeader().setVisible(False) self.table.horizontalHeader().setVisible(False) self.table.setFixedWidth(250) self.table.setColumnWidth(Column.item, 200) self.table.setColumnWidth(Column.remove, 23) self.table.setColumnWidth(Column.scheme, 25) self.table.clicked.connect(self._table_clicked) self.table.selectionModel().selectionChanged.connect( self._table_selection_changed) self.controlArea.layout().addWidget(self.table) self.last_scheme = None self.scheme_button = gui.button(self.controlArea, self, "Back to Last Scheme", callback=self._show_last_scheme) box = gui.hBox(self.controlArea) box.setContentsMargins(-6, 0, -6, 0) self.save_button = gui.button(box, self, "Save", callback=self.save_report, disabled=True) self.print_button = gui.button(box, self, "Print", callback=self._print_report, disabled=True) class PyBridge(QObject): @pyqtSlot(str) def _select_item(myself, item_id): item = self.table_model.get_item_by_id(item_id) self.table.selectRow( self.table_model.indexFromItem(item).row()) self._change_selected_item(item) @pyqtSlot(str, str) def _add_comment(myself, item_id, value): item = self.table_model.get_item_by_id(item_id) item.comment = value self.report_changed = True self.report_view = WebviewWidget(self.mainArea, bridge=PyBridge(self)) self.mainArea.layout().addWidget(self.report_view) @deprecated("Widgets should not be pickled") def __getstate__(self): rep_dict = self.__dict__.copy() for key in ('_OWWidget__env', 'controlArea', 'mainArea', 'report_view', 'table', 'table_model'): del rep_dict[key] items_len = self.table_model.rowCount() return rep_dict, [self.table_model.item(i) for i in range(items_len)] @deprecated("Widgets should not be pickled") def __setstate__(self, state): rep_dict, items = state self.__dict__.update(rep_dict) self._setup_ui_() for i in range(len(items)): item = items[i] self.table_model.add_item( ReportItem(item.name, item.html, item.scheme, item.module, item.icon_name, item.comment)) def _table_clicked(self, index): if index.column() == Column.remove: self._remove_item(index.row()) indexes = self.table.selectionModel().selectedIndexes() if indexes: item = self.table_model.item(indexes[0].row()) self._scroll_to_item(item) self._change_selected_item(item) if index.column() == Column.scheme: self._show_scheme(index.row()) def _table_selection_changed(self, new_selection, _): if new_selection.indexes(): item = self.table_model.item(new_selection.indexes()[0].row()) self._scroll_to_item(item) self._change_selected_item(item) def _remove_item(self, row): self.table_model.removeRow(row) self._empty_report() self.report_changed = True self._build_html() def clear(self): self.table_model.clear() self._empty_report() self.report_changed = True self._build_html() def _add_item(self, widget): name = widget.get_widget_name_extension() name = "{} - {}".format(widget.name, name) if name else widget.name item = ReportItem(name, widget.report_html, self._get_scheme(), widget.__module__, widget.icon) self.table_model.add_item(item) self._empty_report() self.report_changed = True return item def _empty_report(self): # disable save and print if no reports self.save_button.setEnabled(self.table_model.rowCount()) self.print_button.setEnabled(self.table_model.rowCount()) def _build_html(self): html = self.report_html_template html += "<body>" for i in range(self.table_model.rowCount()): item = self.table_model.item(i) html += "<div id='{}' class='normal' " \ "onClick='pybridge._select_item(this.id)'>{}<div " \ "class='textwrapper'><textarea " \ "placeholder='Write a comment...'" \ "onInput='this.innerHTML = this.value;" \ "pybridge._add_comment(this.parentNode.parentNode.id, this.value);'" \ ">{}</textarea></div>" \ "</div>".format(item.id, item.html, item.comment) html += "</body></html>" self.report_view.setHtml(html) def _scroll_to_item(self, item): self.report_view.evalJS( "document.getElementById('{}').scrollIntoView();".format(item.id)) def _change_selected_item(self, item): self.report_view.evalJS( "var sel_el = document.getElementsByClassName('selected')[0]; " "if (sel_el.id != {}) " " sel_el.className = 'normal';".format(item.id)) self.report_view.evalJS( "document.getElementById('{}').className = 'selected';".format( item.id)) self.report_changed = True def make_report(self, widget): item = self._add_item(widget) self._build_html() self._scroll_to_item(item) self.table.selectRow(self.table_model.rowCount() - 1) def _get_scheme(self): canvas = self.get_canvas_instance() return canvas.get_scheme_xml() if canvas else None def _show_scheme(self, row): scheme = self.table_model.item(row).scheme canvas = self.get_canvas_instance() if canvas: document = canvas.current_document() if document.isModifiedStrict(): self.last_scheme = canvas.get_scheme_xml() self._load_scheme(scheme) def _show_last_scheme(self): if self.last_scheme: self._load_scheme(self.last_scheme) def _load_scheme(self, contents): # forcibly load the contents into the associated CanvasMainWindow # instance if one exists. Preserve `self` as the designated report. canvas = self.get_canvas_instance() if canvas is not None: document = canvas.current_document() old = document.scheme() if old.has_report() and old.report_view() is self: # remove self so it is not closed old.set_report_view(None) canvas.load_scheme_xml(contents) scheme = canvas.current_document().scheme() scheme.set_report_view(self) def save_report(self): """Save report""" formats = OrderedDict( (('HTML (*.html)', '.html'), ('PDF (*.pdf)', '.pdf'), ('Report (*.report)', '.report'))) filename, selected_format = QFileDialog.getSaveFileName( self, "Save Report", self.save_dir, ';;'.join(formats.keys())) if not filename: return QDialog.Rejected # Set appropriate extension if not set by the user expect_ext = formats[selected_format] if not filename.endswith(expect_ext): filename += expect_ext self.save_dir = os.path.dirname(filename) self.saveSettings() _, extension = os.path.splitext(filename) if extension == ".pdf": printer = QPrinter() printer.setPageSize(QPrinter.A4) printer.setOutputFormat(QPrinter.PdfFormat) printer.setOutputFileName(filename) self._print_to_printer(printer) elif extension == ".report": self.save(filename) else: def save_html(contents): try: with open(filename, "w", encoding="utf-8") as f: f.write(contents) except PermissionError: self.permission_error(filename) save_html(self.report_view.html()) self.report_changed = False return QDialog.Accepted def _print_to_printer(self, printer): filename = printer.outputFileName() if filename: try: # QtWebEngine return self.report_view.page().printToPdf(filename) except AttributeError: try: # QtWebKit return self.report_view.print_(printer) except AttributeError: # QtWebEngine 5.6 pass # Fallback to printing widget as an image self.report_view.render(printer) def _print_report(self): printer = QPrinter() print_dialog = QPrintDialog(printer, self) print_dialog.setWindowTitle("Print report") if print_dialog.exec_() != QDialog.Accepted: return self._print_to_printer(printer) def save(self, filename): attributes = {} for key in ('last_scheme', 'open_dir'): attributes[key] = getattr(self, key, None) items = [ self.table_model.item(i) for i in range(self.table_model.rowCount()) ] report = dict(__version__=1, attributes=attributes, items=items) try: with open(filename, 'wb') as f: pickle.dump(report, f) except PermissionError: self.permission_error(filename) @classmethod def load(cls, filename): with open(filename, 'rb') as f: report = pickle.load(f) if not isinstance(report, dict): return report self = cls() self.__dict__.update(report['attributes']) for item in report['items']: self.table_model.add_item( ReportItem(item.name, item.html, item.scheme, item.module, item.icon_name, item.comment)) return self def permission_error(self, filename): message_critical( self.tr("Permission error when trying to write report."), title=self.tr("Error"), informative_text=self.tr("Permission error occurred " "while saving '{}'.").format(filename), exc_info=True, parent=self) log.error("PermissionError when trying to write report.", exc_info=True) def is_empty(self): return not self.table_model.rowCount() def is_changed(self): return self.report_changed @staticmethod def set_instance(report): warnings.warn("OWReport.set_instance is deprecated", DeprecationWarning, stacklevel=2) app_inst = QApplication.instance() app_inst._report_window = report @staticmethod def get_instance(): warnings.warn("OWReport.get_instance is deprecated", DeprecationWarning, stacklevel=2) app_inst = QApplication.instance() if not hasattr(app_inst, "_report_window"): report = OWReport() app_inst._report_window = report return app_inst._report_window def get_canvas_instance(self): # type: () -> Optional[CanvasMainWindow] """ Return a CanvasMainWindow instance to which this report is attached. Return None if not associated with any window. Returns ------- window : Optional[CanvasMainWindow] """ # Run up the parent/window chain parent = self.parent() if parent is not None: window = parent.window() if isinstance(window, CanvasMainWindow): return window return None def copy_to_clipboard(self): self.report_view.triggerPageAction(self.report_view.page().Copy)
def __init__(self): super().__init__() self.table = None self._html = None def _loadFinished(is_ok): if is_ok: QTimer.singleShot(1, lambda: setattr(self, '_html', self.webview.html())) self.webview = WebviewWidget(loadFinished=_loadFinished) vb = gui.vBox(self.controlArea, 'Import Data') hb = gui.hBox(vb) self.combo = combo = URLComboBox( hb, self.recent, editable=True, minimumWidth=400, insertPolicy=QComboBox.InsertAtTop, toolTip='Format: ' + VALID_URL_HELP, editTextChanged=self.is_valid_url, # Indirect via QTimer because calling wait() -> processEvents, # while our currentIndexChanged event hadn't yet finished. # Avoids calling handler twice. currentIndexChanged=lambda: QTimer.singleShot(1, self.load_url)) hb.layout().addWidget(QLabel('Public link URL:', hb)) hb.layout().addWidget(combo) hb.layout().setStretch(1, 2) RELOAD_TIMES = ( ('No reload',), ('5 s', 5000), ('10 s', 10000), ('30 s', 30000), ('1 min', 60*1000), ('2 min', 2*60*1000), ('5 min', 5*60*1000), ) reload_timer = QTimer(self, timeout=lambda: self.load_url(from_reload=True)) def _on_reload_changed(): if self.reload_idx == 0: reload_timer.stop() return reload_timer.start(RELOAD_TIMES[self.reload_idx][1]) gui.comboBox(vb, self, 'reload_idx', label='Reload every:', orientation=Qt.Horizontal, items=[i[0] for i in RELOAD_TIMES], callback=_on_reload_changed) box = gui.widgetBox(self.controlArea, "Columns (Double-click to edit)") self.domain_editor = DomainEditor(self) editor_model = self.domain_editor.model() def editorDataChanged(): self.apply_domain_edit() self.commit() editor_model.dataChanged.connect(editorDataChanged) box.layout().addWidget(self.domain_editor) box = gui.widgetBox(self.controlArea, "Info", addSpace=True) info = self.data_info = gui.widgetLabel(box, '') info.setWordWrap(True) self.controlArea.layout().addStretch(1) gui.auto_commit(self.controlArea, self, 'autocommit', label='Commit') self.set_info()
class OWReport(OWWidget): name = "Report" save_dir = Setting("") open_dir = Setting("") def __init__(self): super().__init__() self._setup_ui_() self.report_changed = False index_file = pkg_resources.resource_filename(__name__, "index.html") with open(index_file, "r") as f: self.report_html_template = f.read() def _setup_ui_(self): self.table_model = ReportItemModel(0, len(Column.__members__)) self.table = ReportTable(self.controlArea) self.table.setModel(self.table_model) self.table.setShowGrid(False) self.table.setSelectionBehavior(QTableView.SelectRows) self.table.setSelectionMode(QTableView.SingleSelection) self.table.setWordWrap(False) self.table.setMouseTracking(True) self.table.verticalHeader().setSectionResizeMode(QHeaderView.Fixed) self.table.verticalHeader().setDefaultSectionSize(20) self.table.verticalHeader().setVisible(False) self.table.horizontalHeader().setVisible(False) self.table.setFixedWidth(250) self.table.setColumnWidth(Column.item, 200) self.table.setColumnWidth(Column.remove, 23) self.table.setColumnWidth(Column.scheme, 25) self.table.clicked.connect(self._table_clicked) self.table.selectionModel().selectionChanged.connect( self._table_selection_changed) self.controlArea.layout().addWidget(self.table) self.last_scheme = None self.scheme_button = gui.button( self.controlArea, self, "Back to Last Scheme", callback=self._show_last_scheme ) box = gui.hBox(self.controlArea) box.setContentsMargins(-6, 0, -6, 0) self.save_button = gui.button( box, self, "Save", callback=self.save_report ) self.print_button = gui.button( box, self, "Print", callback=self._print_report ) class PyBridge(QObject): @pyqtSlot(str) def _select_item(myself, item_id): item = self.table_model.get_item_by_id(item_id) self.table.selectRow(self.table_model.indexFromItem(item).row()) self._change_selected_item(item) @pyqtSlot(str, str) def _add_comment(myself, item_id, value): item = self.table_model.get_item_by_id(item_id) item.comment = value self.report_changed = True self.report_view = WebviewWidget(self.mainArea, bridge=PyBridge(self)) self.mainArea.layout().addWidget(self.report_view) @deprecated("Widgets should not be pickled") def __getstate__(self): rep_dict = self.__dict__.copy() for key in ('_OWWidget__env', 'controlArea', 'mainArea', 'report_view', 'table', 'table_model'): del rep_dict[key] items_len = self.table_model.rowCount() return rep_dict, [self.table_model.item(i) for i in range(items_len)] @deprecated("Widgets should not be pickled") def __setstate__(self, state): rep_dict, items = state self.__dict__.update(rep_dict) self._setup_ui_() for i in range(len(items)): item = items[i] self.table_model.add_item( ReportItem(item.name, item.html, item.scheme, item.module, item.icon_name, item.comment) ) def _table_clicked(self, index): if index.column() == Column.remove: self._remove_item(index.row()) indexes = self.table.selectionModel().selectedIndexes() if indexes: item = self.table_model.item(indexes[0].row()) self._scroll_to_item(item) self._change_selected_item(item) if index.column() == Column.scheme: self._show_scheme(index.row()) def _table_selection_changed(self, new_selection, _): if new_selection.indexes(): item = self.table_model.item(new_selection.indexes()[0].row()) self._scroll_to_item(item) self._change_selected_item(item) def _remove_item(self, row): self.table_model.removeRow(row) self.report_changed = True self._build_html() def clear(self): self.table_model.clear() self.report_changed = True self._build_html() def _add_item(self, widget): name = widget.get_widget_name_extension() name = "{} - {}".format(widget.name, name) if name else widget.name item = ReportItem(name, widget.report_html, self._get_scheme(), widget.__module__, widget.icon) self.table_model.add_item(item) self.report_changed = True return item def _build_html(self): html = self.report_html_template html += "<body>" for i in range(self.table_model.rowCount()): item = self.table_model.item(i) html += "<div id='{}' class='normal' " \ "onClick='pybridge._select_item(this.id)'>{}<div " \ "class='textwrapper'><textarea " \ "placeholder='Write a comment...'" \ "onInput='this.innerHTML = this.value;" \ "pybridge._add_comment(this.parentNode.parentNode.id, this.value);'" \ ">{}</textarea></div>" \ "</div>".format(item.id, item.html, item.comment) html += "</body></html>" self.report_view.setHtml(html) def _scroll_to_item(self, item): self.report_view.evalJS( "document.getElementById('{}').scrollIntoView();".format(item.id) ) def _change_selected_item(self, item): self.report_view.evalJS( "var sel_el = document.getElementsByClassName('selected')[0]; " "if (sel_el.id != {}) " " sel_el.className = 'normal';".format(item.id)) self.report_view.evalJS( "document.getElementById('{}').className = 'selected';" .format(item.id)) self.report_changed = True def make_report(self, widget): item = self._add_item(widget) self._build_html() self._scroll_to_item(item) self.table.selectRow(self.table_model.rowCount() - 1) def _get_scheme(self): canvas = self.get_canvas_instance() return canvas.get_scheme_xml() if canvas else None def _show_scheme(self, row): scheme = self.table_model.item(row).scheme canvas = self.get_canvas_instance() if canvas: document = canvas.current_document() if document.isModifiedStrict(): self.last_scheme = canvas.get_scheme_xml() canvas.load_scheme_xml(scheme) def _show_last_scheme(self): if self.last_scheme: canvas = self.get_canvas_instance() if canvas: canvas.load_scheme_xml(self.last_scheme) def save_report(self): """Save report""" filename, _ = QFileDialog.getSaveFileName( self, "Save Report", self.save_dir, "HTML (*.html);;PDF (*.pdf);;Report (*.report)") if not filename: return QDialog.Rejected self.save_dir = os.path.dirname(filename) self.saveSettings() _, extension = os.path.splitext(filename) if extension == ".pdf": printer = QPrinter() printer.setPageSize(QPrinter.A4) printer.setOutputFormat(QPrinter.PdfFormat) printer.setOutputFileName(filename) self.report_view.print_(printer) elif extension == ".report": self.save(filename) else: def save_html(contents): try: with open(filename, "w", encoding="utf-8") as f: f.write(contents) except PermissionError: self.permission_error(filename) save_html(self.report_view.html()) self.report_changed = False return QDialog.Accepted def _print_report(self): printer = QPrinter() print_dialog = QPrintDialog(printer, self) print_dialog.setWindowTitle("Print report") if print_dialog.exec_() != QDialog.Accepted: return self.report_view.print_(printer) def open_report(self): filename, _ = QFileDialog.getOpenFileName( self, "Open Report", self.open_dir, "Report (*.report)") if not filename: return self.report_changed = False self.open_dir = os.path.dirname(filename) self.saveSettings() try: report = self.load(filename) except (IOError, AttributeError, pickle.UnpicklingError) as e: message_critical( self.tr("Could not load an Orange Report file"), title=self.tr("Error"), informative_text=self.tr("Error occurred " "while loading '{}'.").format(filename), exc_info=True, parent=self) log.error(str(e), exc_info=True) return self.set_instance(report) self = report self._build_html() self.table.selectRow(0) self.show() self.raise_() def save(self, filename): attributes = {} for key in ('last_scheme', 'open_dir'): attributes[key] = getattr(self, key, None) items = [self.table_model.item(i) for i in range(self.table_model.rowCount())] report = dict(__version__=1, attributes=attributes, items=items) try: with open(filename, 'wb') as f: pickle.dump(report, f) except PermissionError: self.permission_error(filename) @classmethod def load(cls, filename): with open(filename, 'rb') as f: report = pickle.load(f) if not isinstance(report, dict): return report self = cls() self.__dict__.update(report['attributes']) for item in report['items']: self.table_model.add_item( ReportItem(item.name, item.html, item.scheme, item.module, item.icon_name, item.comment) ) return self def permission_error(self, filename): message_critical( self.tr("Permission error when trying to write report."), title=self.tr("Error"), informative_text=self.tr("Permission error occurred " "while saving '{}'.").format(filename), exc_info=True, parent=self) log.error("PermissionError when trying to write report.", exc_info=True) def is_empty(self): return not self.table_model.rowCount() def is_changed(self): return self.report_changed @staticmethod def set_instance(report): app_inst = QApplication.instance() app_inst._report_window = report @staticmethod def get_instance(): app_inst = QApplication.instance() if not hasattr(app_inst, "_report_window"): report = OWReport() app_inst._report_window = report return app_inst._report_window @staticmethod def get_canvas_instance(): for widget in QApplication.topLevelWidgets(): if isinstance(widget, CanvasMainWindow): return widget
def test_base(self): w = WebviewWidget() w.evalJS('document.write("foo");') SVG = '<svg xmlns:dc="...">asd</svg>' w.onloadJS('''document.write('{}');'''.format(SVG)) w.setUrl(SOME_URL) svg = self.process_events(lambda: w.svg()) self.assertEqual(svg, SVG) self.process_events(until=lambda: 'foo' in w.html()) html = '<svg xmlns:dc="...">asd</svg>' self.assertEqual( w.html(), '<html><head></head><body>{}</body></html>'.format( # WebKit evaluates first document.write first, whereas # WebEngine evaluates onloadJS first 'foo' + html if HAVE_WEBKIT else html + 'foo'))
from os.path import dirname from unittest import skip from AnyQt.QtCore import Qt, QObject, pyqtSlot from AnyQt.QtWidgets import QDialog from AnyQt.QtTest import QTest from Orange.widgets.tests.base import WidgetTest from Orange.widgets.utils.webview import WebviewWidget, HAVE_WEBKIT, wait SOME_URL = WebviewWidget.toFileURL(dirname(__file__)) @skip("Times out on Travis") class WebviewWidgetTest(WidgetTest): def test_base(self): w = WebviewWidget() w.evalJS('document.write("foo");') SVG = '<svg xmlns:dc="...">asd</svg>' w.onloadJS('''document.write('{}');'''.format(SVG)) w.setUrl(SOME_URL) svg = self.process_events(lambda: w.svg()) self.assertEqual(svg, SVG) self.process_events(until=lambda: 'foo' in w.html()) html = '<svg xmlns:dc="...">asd</svg>' self.assertEqual( w.html(), '<html><head></head><body>{}</body></html>'.format( # WebKit evaluates first document.write first, whereas
class OW1ka(widget.OWWidget): name = "EnKlik Anketa" description = "Import data from EnKlikAnketa (1ka.si) public URL." icon = "icons/1ka.svg" priority = 30 outputs = [("Data", Table)] want_main_area = False resizing_enabled = False settingsHandler = settings.PerfectDomainContextHandler( match_values=settings.PerfectDomainContextHandler.MATCH_VALUES_ALL ) recent = settings.Setting([]) reload_idx = settings.Setting(0) autocommit = settings.Setting(True) domain_editor = settings.SettingProvider(DomainEditor) UserAdviceMessages = [ widget.Message( 'You can import data from public links to 1ka surveys results. ' 'Click to learn more on how to get a shareable public link URL for ' '1ka surveys that you manage.', 'public-link', icon=widget.Message.Information, moreurl='http://english.1ka.si/db/24/468/Guides/Public_link_to_access_data_and_analysis/' ), ] class Error(widget.OWWidget.Error): net_error = widget.Msg("Couldn't load data: {}. Ensure network connection, firewall ...") parse_error = widget.Msg("Couldn't parse data: {}. Ensure well-formatted data or submit a bug report.") invalid_url = widget.Msg('Invalid URL. Public shareable link should match: ' + VALID_URL_HELP) data_is_anal = widget.Msg("The provided URL is a public link to 'Analysis'. Need public link to 'Data'.") class Information(widget.OWWidget.Information): response_data_empty = widget.Msg('Response data is empty. Get some responses first.') def __init__(self): super().__init__() self.table = None self._html = None def _loadFinished(is_ok): if is_ok: QTimer.singleShot(1, lambda: setattr(self, '_html', self.webview.html())) self.webview = WebviewWidget(loadFinished=_loadFinished) vb = gui.vBox(self.controlArea, 'Import Data') hb = gui.hBox(vb) self.combo = combo = URLComboBox( hb, self.recent, editable=True, minimumWidth=400, insertPolicy=QComboBox.InsertAtTop, toolTip='Format: ' + VALID_URL_HELP, editTextChanged=self.is_valid_url, # Indirect via QTimer because calling wait() -> processEvents, # while our currentIndexChanged event hadn't yet finished. # Avoids calling handler twice. currentIndexChanged=lambda: QTimer.singleShot(1, self.load_url)) hb.layout().addWidget(QLabel('Public link URL:', hb)) hb.layout().addWidget(combo) hb.layout().setStretch(1, 2) RELOAD_TIMES = ( ('No reload',), ('5 s', 5000), ('10 s', 10000), ('30 s', 30000), ('1 min', 60*1000), ('2 min', 2*60*1000), ('5 min', 5*60*1000), ) reload_timer = QTimer(self, timeout=lambda: self.load_url(from_reload=True)) def _on_reload_changed(): if self.reload_idx == 0: reload_timer.stop() return reload_timer.start(RELOAD_TIMES[self.reload_idx][1]) gui.comboBox(vb, self, 'reload_idx', label='Reload every:', orientation=Qt.Horizontal, items=[i[0] for i in RELOAD_TIMES], callback=_on_reload_changed) box = gui.widgetBox(self.controlArea, "Columns (Double-click to edit)") self.domain_editor = DomainEditor(self) editor_model = self.domain_editor.model() def editorDataChanged(): self.apply_domain_edit() self.commit() editor_model.dataChanged.connect(editorDataChanged) box.layout().addWidget(self.domain_editor) box = gui.widgetBox(self.controlArea, "Info", addSpace=True) info = self.data_info = gui.widgetLabel(box, '') info.setWordWrap(True) self.controlArea.layout().addStretch(1) gui.auto_commit(self.controlArea, self, 'autocommit', label='Commit') self.set_info() def set_combo_items(self): self.combo.clear() for sheet in self.recent: self.combo.addItem(sheet.name, sheet.url) def commit(self): self.send('Data', self.table) def is_valid_url(self, url): if is_valid_url(url): self.Error.invalid_url.clear() return True self.Error.invalid_url() QToolTip.showText(self.combo.mapToGlobal(QPoint(0, 0)), self.combo.toolTip()) def load_url(self, from_reload=False): self.closeContext() self.domain_editor.set_domain(None) url = self.combo.currentText() if not self.is_valid_url(url): self.table = None self.commit() return if url not in self.recent: self.recent.insert(0, url) prev_table = self.table with self.progressBar(3) as progress: try: self._html = None self.webview.setUrl(url) wait(until=lambda: self._html is not None) progress.advance() # Wait some seconds for discrete labels to have loaded via AJAX, # then re-query HTML. # *Webview.loadFinished doesn't guarantee it sufficiently try: wait(until=lambda: False, timeout=1200) except TimeoutError: pass progress.advance() html = self.webview.html() except Exception as e: log.exception("Couldn't load data from: %s", url) self.Error.net_error(try_(lambda: e.args[0], '')) self.table = None else: self.Error.clear() self.Information.clear() self.table = None try: table = self.table = self.table_from_html(html) except DataEmptyError: self.Information.response_data_empty() except DataIsAnalError: self.Error.data_is_anal() except Exception as e: log.exception('Parsing error: %s', url) self.Error.parse_error(try_(lambda: e.args[0], '')) else: self.openContext(table.domain) self.combo.setTitleFor(self.combo.currentIndex(), table.name) def _equal(data1, data2): NAN = float('nan') return (try_(lambda: data1.checksum(), NAN) == try_(lambda: data2.checksum(), NAN)) self._orig_table = self.table self.apply_domain_edit() if not (from_reload and _equal(prev_table, self.table)): self.commit() def apply_domain_edit(self): data = self._orig_table if data is None: self.set_info() return domain, cols = self.domain_editor.get_domain(data.domain, data) # Copied verbatim from OWFile if not (domain.variables or domain.metas): table = None else: X, y, m = cols table = Table.from_numpy(domain, X, y, m, data.W) table.name = data.name table.ids = np.array(data.ids) table.attributes = getattr(data, 'attributes', {}) self.table = table self.set_info() DATETIME_VAR = 'Paradata (insert)' def table_from_html(self, html): soup = BeautifulSoup(html, 'html.parser') try: html_table = soup.find_all('table')[-1] except IndexError: raise DataEmptyError if '<h2>Anal' in html or 'div_analiza_' in html: raise DataIsAnalError def _header_row_strings(row): return chain.from_iterable( repeat(th.get_text(), int(th.get('colspan') or 1)) for th in html_table.select('thead tr:nth-of-type(%d) th[title]' % row)) # self.DATETIME_VAR (available when Paradata is enabled in 1ka UI) # should match this variable name format header = [th1.rstrip(':') + ('' if th3 == th1 else ' ({})').format(th3.rstrip(':')) for th1, th3 in zip(_header_row_strings(1), _header_row_strings(3))] values = [[(# If no span, feature is a number or a text field td.get_text() if td.span is None else # If have span, it's a number, but if negative, replace with NaN '' if td.contents[0].strip().startswith('-') else # Else if span, the number is its code, but we want its value td.span.get_text()[1:-1]) for td in tr.select('td') if 'data_uid' not in td.get('class', ())] for tr in html_table.select('tbody tr')] # Save parsed values into in-mem file for default values processing buffer = StringIO() writer = csv.writer(buffer, delimiter='\t') writer.writerow(header) writer.writerows(values) buffer.flush() buffer.seek(0) data = TabReader(buffer).read() title = soup.select('body h2:nth-of-type(1)')[0].get_text().split(': ', maxsplit=1)[-1] data.name = title return data def set_info(self): data = self.table if data is None: self.data_info.setText('No spreadsheet loaded.') return text = "{}\n\n{} instance(s), {} feature(s), {} meta attribute(s)\n".format( data.name, len(data), len(data.domain.attributes), len(data.domain.metas)) text += try_(lambda: '\nFirst entry: {}' '\nLast entry: {}'.format(data[0, self.DATETIME_VAR], data[-1, self.DATETIME_VAR]), '') self.data_info.setText(text)
from os.path import dirname from AnyQt.QtCore import Qt, QObject, pyqtSlot from AnyQt.QtWidgets import QDialog, qApp from AnyQt.QtTest import QTest from Orange.widgets.tests.base import WidgetTest from Orange.widgets.utils.webview import WebviewWidget SOME_URL = WebviewWidget.toFileURL(dirname(__file__)) class WebviewWidgetTest(WidgetTest): def test_base(self): w = WebviewWidget() w.evalJS('document.write("foo");') SVG = '<svg xmlns:dc="...">asd</svg>' w.onloadJS('''document.write('{}');'''.format(SVG)) w.setUrl(SOME_URL) svg = None while svg is None: try: svg = w.svg() break except ValueError: qApp.processEvents() self.assertEqual(svg, SVG) self.assertEqual( w.html(), '<html><head></head><body>foo<svg xmlns:dc="...">asd</svg></body></html>')
class OW1ka(widget.OWWidget): name = "EnKlik Anketa" description = "Import data from EnKlikAnketa (1ka.si) public URL." icon = "icons/1ka.svg" priority = 200 class Outputs: data = Output("Data", Table) want_main_area = False resizing_enabled = False settingsHandler = settings.PerfectDomainContextHandler( match_values=settings.PerfectDomainContextHandler.MATCH_VALUES_ALL) recent = settings.Setting([]) reload_idx = settings.Setting(0) autocommit = settings.Setting(True) domain_editor = settings.SettingProvider(DomainEditor) UserAdviceMessages = [ widget.Message( 'You can import data from public links to 1ka surveys results. ' 'Click to learn more on how to get a shareable public link URL for ' '1ka surveys that you manage.', 'public-link', icon=widget.Message.Information, moreurl= 'http://english.1ka.si/db/24/468/Guides/Public_link_to_access_data_and_analysis/' ), ] class Error(widget.OWWidget.Error): net_error = widget.Msg( "Couldn't load data: {}. Ensure network connection, firewall ...") parse_error = widget.Msg( "Couldn't parse data: {}. Ensure well-formatted data or submit a bug report." ) invalid_url = widget.Msg( 'Invalid URL. Public shareable link should match: ' + VALID_URL_HELP) data_is_anal = widget.Msg( "The provided URL is a public link to 'Analysis'. Need public link to 'Data'." ) class Information(widget.OWWidget.Information): response_data_empty = widget.Msg( 'Response data is empty. Get some responses first.') def __init__(self): super().__init__() self.table = None self._html = None def _loadFinished(is_ok): if is_ok: QTimer.singleShot( 1, lambda: setattr(self, '_html', self.webview.html())) self.webview = WebviewWidget(loadFinished=_loadFinished) vb = gui.vBox(self.controlArea, 'Import Data') hb = gui.hBox(vb) self.combo = combo = URLComboBox( hb, self.recent, editable=True, minimumWidth=400, insertPolicy=QComboBox.InsertAtTop, toolTip='Format: ' + VALID_URL_HELP, editTextChanged=self.is_valid_url, # Indirect via QTimer because calling wait() -> processEvents, # while our currentIndexChanged event hadn't yet finished. # Avoids calling handler twice. currentIndexChanged=lambda: QTimer.singleShot(1, self.load_url)) hb.layout().addWidget(QLabel('Public link URL:', hb)) hb.layout().addWidget(combo) hb.layout().setStretch(1, 2) RELOAD_TIMES = ( ('No reload', ), ('5 s', 5000), ('10 s', 10000), ('30 s', 30000), ('1 min', 60 * 1000), ('2 min', 2 * 60 * 1000), ('5 min', 5 * 60 * 1000), ) reload_timer = QTimer(self, timeout=lambda: self.load_url(from_reload=True)) def _on_reload_changed(): if self.reload_idx == 0: reload_timer.stop() return reload_timer.start(RELOAD_TIMES[self.reload_idx][1]) gui.comboBox(vb, self, 'reload_idx', label='Reload every:', orientation=Qt.Horizontal, items=[i[0] for i in RELOAD_TIMES], callback=_on_reload_changed) box = gui.widgetBox(self.controlArea, "Columns (Double-click to edit)") self.domain_editor = DomainEditor(self) editor_model = self.domain_editor.model() def editorDataChanged(): self.apply_domain_edit() self.commit() editor_model.dataChanged.connect(editorDataChanged) box.layout().addWidget(self.domain_editor) box = gui.widgetBox(self.controlArea, "Info", addSpace=True) info = self.data_info = gui.widgetLabel(box, '') info.setWordWrap(True) self.controlArea.layout().addStretch(1) gui.auto_commit(self.controlArea, self, 'autocommit', label='Commit') self.set_info() def set_combo_items(self): self.combo.clear() for sheet in self.recent: self.combo.addItem(sheet.name, sheet.url) def commit(self): self.Outputs.data.send(self.table) def is_valid_url(self, url): if is_valid_url(url): self.Error.invalid_url.clear() return True self.Error.invalid_url() QToolTip.showText(self.combo.mapToGlobal(QPoint(0, 0)), self.combo.toolTip()) def load_url(self, from_reload=False): self.closeContext() self.domain_editor.set_domain(None) url = self.combo.currentText() if not self.is_valid_url(url): self.table = None self.commit() return if url not in self.recent: self.recent.insert(0, url) prev_table = self.table with self.progressBar(3) as progress: try: self._html = None self.webview.setUrl(url) wait(until=lambda: self._html is not None) progress.advance() # Wait some seconds for discrete labels to have loaded via AJAX, # then re-query HTML. # *Webview.loadFinished doesn't guarantee it sufficiently try: wait(until=lambda: False, timeout=1200) except TimeoutError: pass progress.advance() html = self.webview.html() except Exception as e: log.exception("Couldn't load data from: %s", url) self.Error.net_error(try_(lambda: e.args[0], '')) self.table = None else: self.Error.clear() self.Information.clear() self.table = None try: table = self.table = self.table_from_html(html) except DataEmptyError: self.Information.response_data_empty() except DataIsAnalError: self.Error.data_is_anal() except Exception as e: log.exception('Parsing error: %s', url) self.Error.parse_error(try_(lambda: e.args[0], '')) else: self.openContext(table.domain) self.combo.setTitleFor(self.combo.currentIndex(), table.name) def _equal(data1, data2): NAN = float('nan') return (try_(lambda: data1.checksum(), NAN) == try_(lambda: data2.checksum(), NAN)) self._orig_table = self.table self.apply_domain_edit() if not (from_reload and _equal(prev_table, self.table)): self.commit() def apply_domain_edit(self): data = self._orig_table if data is None: self.set_info() return domain, cols = self.domain_editor.get_domain(data.domain, data) # Copied verbatim from OWFile if not (domain.variables or domain.metas): table = None else: X, y, m = cols table = Table.from_numpy(domain, X, y, m, data.W) table.name = data.name table.ids = np.array(data.ids) table.attributes = getattr(data, 'attributes', {}) self.table = table self.set_info() DATETIME_VAR = 'Paradata (insert)' def table_from_html(self, html): soup = BeautifulSoup(html, 'html.parser') try: html_table = soup.find_all('table')[-1] except IndexError: raise DataEmptyError if '<h2>Anal' in html or 'div_analiza_' in html: raise DataIsAnalError def _header_row_strings(row): return chain.from_iterable( repeat(th.get_text(), int(th.get('colspan') or 1)) for th in html_table.select('thead tr:nth-of-type(%d) th[title]' % row)) # self.DATETIME_VAR (available when Paradata is enabled in 1ka UI) # should match this variable name format header = [ th1.rstrip(':') + ('' if th3 == th1 else ' ({})').format(th3.rstrip(':')) for th1, th3 in zip(_header_row_strings(1), _header_row_strings(3)) ] values = [ [ ( # If no span, feature is a number or a text field td.get_text() if td.span is None else # If have span, it's a number, but if negative, replace with NaN '' if td.contents[0].strip().startswith('-') else # Else if span, the number is its code, but we want its value td.span.get_text()[1:-1]) for td in tr.select('td') if 'data_uid' not in td.get('class', ()) ] for tr in html_table.select('tbody tr') ] # Save parsed values into in-mem file for default values processing buffer = StringIO() writer = csv.writer(buffer, delimiter='\t') writer.writerow(header) writer.writerows(values) buffer.flush() buffer.seek(0) data = TabReader(buffer).read() title = soup.select('body h2:nth-of-type(1)')[0].get_text().split( ': ', maxsplit=1)[-1] data.name = title return data def set_info(self): data = self.table if data is None: self.data_info.setText('No spreadsheet loaded.') return text = "{}\n\n{} instance(s), {} feature(s), {} meta attribute(s)\n".format( data.name, len(data), len(data.domain.attributes), len(data.domain.metas)) text += try_( lambda: '\nFirst entry: {}' '\nLast entry: {}'.format(data[0, self.DATETIME_VAR], data[ -1, self.DATETIME_VAR]), '') self.data_info.setText(text)
def __init__(self): super().__init__() self.table = None self._html = None def _loadFinished(is_ok): if is_ok: QTimer.singleShot( 1, lambda: setattr(self, '_html', self.webview.html())) self.webview = WebviewWidget(loadFinished=_loadFinished) vb = gui.vBox(self.controlArea, 'Import Data') hb = gui.hBox(vb) self.combo = combo = URLComboBox( hb, self.recent, editable=True, minimumWidth=400, insertPolicy=QComboBox.InsertAtTop, toolTip='Format: ' + VALID_URL_HELP, editTextChanged=self.is_valid_url, # Indirect via QTimer because calling wait() -> processEvents, # while our currentIndexChanged event hadn't yet finished. # Avoids calling handler twice. currentIndexChanged=lambda: QTimer.singleShot(1, self.load_url)) hb.layout().addWidget(QLabel('Public link URL:', hb)) hb.layout().addWidget(combo) hb.layout().setStretch(1, 2) RELOAD_TIMES = ( ('No reload', ), ('5 s', 5000), ('10 s', 10000), ('30 s', 30000), ('1 min', 60 * 1000), ('2 min', 2 * 60 * 1000), ('5 min', 5 * 60 * 1000), ) reload_timer = QTimer(self, timeout=lambda: self.load_url(from_reload=True)) def _on_reload_changed(): if self.reload_idx == 0: reload_timer.stop() return reload_timer.start(RELOAD_TIMES[self.reload_idx][1]) gui.comboBox(vb, self, 'reload_idx', label='Reload every:', orientation=Qt.Horizontal, items=[i[0] for i in RELOAD_TIMES], callback=_on_reload_changed) box = gui.widgetBox(self.controlArea, "Columns (Double-click to edit)") self.domain_editor = DomainEditor(self) editor_model = self.domain_editor.model() def editorDataChanged(): self.apply_domain_edit() self.commit() editor_model.dataChanged.connect(editorDataChanged) box.layout().addWidget(self.domain_editor) box = gui.widgetBox(self.controlArea, "Info", addSpace=True) info = self.data_info = gui.widgetLabel(box, '') info.setWordWrap(True) self.controlArea.layout().addStretch(1) gui.auto_commit(self.controlArea, self, 'autocommit', label='Commit') self.set_info()
class OWReport(OWWidget): name = "Report" save_dir = Setting("") open_dir = Setting("") def __init__(self): super().__init__() self._setup_ui_() self.report_changed = False index_file = pkg_resources.resource_filename(__name__, "index.html") with open(index_file, "r") as f: self.report_html_template = f.read() def _setup_ui_(self): self.table_model = ReportItemModel(0, len(Column.__members__)) self.table = ReportTable(self.controlArea) self.table.setModel(self.table_model) self.table.setShowGrid(False) self.table.setSelectionBehavior(QTableView.SelectRows) self.table.setSelectionMode(QTableView.SingleSelection) self.table.setWordWrap(False) self.table.setMouseTracking(True) self.table.verticalHeader().setSectionResizeMode(QHeaderView.Fixed) self.table.verticalHeader().setDefaultSectionSize(20) self.table.verticalHeader().setVisible(False) self.table.horizontalHeader().setVisible(False) self.table.setFixedWidth(250) self.table.setColumnWidth(Column.item, 200) self.table.setColumnWidth(Column.remove, 23) self.table.setColumnWidth(Column.scheme, 25) self.table.clicked.connect(self._table_clicked) self.table.selectionModel().selectionChanged.connect( self._table_selection_changed) self.controlArea.layout().addWidget(self.table) self.last_scheme = None self.scheme_button = gui.button(self.controlArea, self, "Back to Last Scheme", callback=self._show_last_scheme) box = gui.hBox(self.controlArea) box.setContentsMargins(-6, 0, -6, 0) self.save_button = gui.button(box, self, "Save", callback=self.save_report) self.print_button = gui.button(box, self, "Print", callback=self._print_report) class PyBridge(QObject): @pyqtSlot(str) def _select_item(myself, item_id): item = self.table_model.get_item_by_id(item_id) self.table.selectRow( self.table_model.indexFromItem(item).row()) self._change_selected_item(item) @pyqtSlot(str, str) def _add_comment(myself, item_id, value): item = self.table_model.get_item_by_id(item_id) item.comment = value self.report_changed = True self.report_view = WebviewWidget(self.mainArea, bridge=PyBridge(self)) self.mainArea.layout().addWidget(self.report_view) @deprecated("Widgets should not be pickled") def __getstate__(self): rep_dict = self.__dict__.copy() for key in ('_OWWidget__env', 'controlArea', 'mainArea', 'report_view', 'table', 'table_model'): del rep_dict[key] items_len = self.table_model.rowCount() return rep_dict, [self.table_model.item(i) for i in range(items_len)] @deprecated("Widgets should not be pickled") def __setstate__(self, state): rep_dict, items = state self.__dict__.update(rep_dict) self._setup_ui_() for i in range(len(items)): item = items[i] self.table_model.add_item( ReportItem(item.name, item.html, item.scheme, item.module, item.icon_name, item.comment)) def _table_clicked(self, index): if index.column() == Column.remove: self._remove_item(index.row()) indexes = self.table.selectionModel().selectedIndexes() if indexes: item = self.table_model.item(indexes[0].row()) self._scroll_to_item(item) self._change_selected_item(item) if index.column() == Column.scheme: self._show_scheme(index.row()) def _table_selection_changed(self, new_selection, _): if new_selection.indexes(): item = self.table_model.item(new_selection.indexes()[0].row()) self._scroll_to_item(item) self._change_selected_item(item) def _remove_item(self, row): self.table_model.removeRow(row) self.report_changed = True self._build_html() def clear(self): self.table_model.clear() self.report_changed = True self._build_html() def _add_item(self, widget): name = widget.get_widget_name_extension() name = "{} - {}".format(widget.name, name) if name else widget.name item = ReportItem(name, widget.report_html, self._get_scheme(), widget.__module__, widget.icon) self.table_model.add_item(item) self.report_changed = True return item def _build_html(self): html = self.report_html_template html += "<body>" for i in range(self.table_model.rowCount()): item = self.table_model.item(i) html += "<div id='{}' class='normal' " \ "onClick='pybridge._select_item(this.id)'>{}<div " \ "class='textwrapper'><textarea " \ "placeholder='Write a comment...'" \ "onInput='this.innerHTML = this.value;" \ "pybridge._add_comment(this.parentNode.parentNode.id, this.value);'" \ ">{}</textarea></div>" \ "</div>".format(item.id, item.html, item.comment) html += "</body></html>" self.report_view.setHtml(html) def _scroll_to_item(self, item): self.report_view.evalJS( "document.getElementById('{}').scrollIntoView();".format(item.id)) def _change_selected_item(self, item): self.report_view.evalJS( "var sel_el = document.getElementsByClassName('selected')[0]; " "if (sel_el.id != {}) " " sel_el.className = 'normal';".format(item.id)) self.report_view.evalJS( "document.getElementById('{}').className = 'selected';".format( item.id)) self.report_changed = True def make_report(self, widget): item = self._add_item(widget) self._build_html() self._scroll_to_item(item) self.table.selectRow(self.table_model.rowCount() - 1) def _get_scheme(self): canvas = self.get_canvas_instance() return canvas.get_scheme_xml() if canvas else None def _show_scheme(self, row): scheme = self.table_model.item(row).scheme canvas = self.get_canvas_instance() if canvas: document = canvas.current_document() if document.isModifiedStrict(): self.last_scheme = canvas.get_scheme_xml() canvas.load_scheme_xml(scheme) def _show_last_scheme(self): if self.last_scheme: canvas = self.get_canvas_instance() if canvas: canvas.load_scheme_xml(self.last_scheme) def save_report(self): """Save report""" filename, _ = QFileDialog.getSaveFileName( self, "Save Report", self.save_dir, "HTML (*.html);;PDF (*.pdf);;Report (*.report)") if not filename: return QDialog.Rejected self.save_dir = os.path.dirname(filename) self.saveSettings() _, extension = os.path.splitext(filename) if extension == ".pdf": printer = QPrinter() printer.setPageSize(QPrinter.A4) printer.setOutputFormat(QPrinter.PdfFormat) printer.setOutputFileName(filename) self.report_view.print_(printer) elif extension == ".report": self.save(filename) else: def save_html(contents): try: with open(filename, "w", encoding="utf-8") as f: f.write(contents) except PermissionError: self.permission_error(filename) save_html(self.report_view.html()) self.report_changed = False return QDialog.Accepted def _print_report(self): printer = QPrinter() print_dialog = QPrintDialog(printer, self) print_dialog.setWindowTitle("Print report") if print_dialog.exec_() != QDialog.Accepted: return self.report_view.print_(printer) def open_report(self): filename, _ = QFileDialog.getOpenFileName(self, "Open Report", self.open_dir, "Report (*.report)") if not filename: return self.report_changed = False self.open_dir = os.path.dirname(filename) self.saveSettings() try: report = self.load(filename) except (IOError, AttributeError, pickle.UnpicklingError) as e: message_critical(self.tr("Could not load an Orange Report file"), title=self.tr("Error"), informative_text=self.tr( "Error occurred " "while loading '{}'.").format(filename), exc_info=True, parent=self) log.error(str(e), exc_info=True) return self.set_instance(report) self = report self._build_html() self.table.selectRow(0) self.show() self.raise_() def save(self, filename): attributes = {} for key in ('last_scheme', 'open_dir'): attributes[key] = getattr(self, key, None) items = [ self.table_model.item(i) for i in range(self.table_model.rowCount()) ] report = dict(__version__=1, attributes=attributes, items=items) try: with open(filename, 'wb') as f: pickle.dump(report, f) except PermissionError: self.permission_error(filename) @classmethod def load(cls, filename): with open(filename, 'rb') as f: report = pickle.load(f) if not isinstance(report, dict): return report self = cls() self.__dict__.update(report['attributes']) for item in report['items']: self.table_model.add_item( ReportItem(item.name, item.html, item.scheme, item.module, item.icon_name, item.comment)) return self def permission_error(self, filename): message_critical( self.tr("Permission error when trying to write report."), title=self.tr("Error"), informative_text=self.tr("Permission error occurred " "while saving '{}'.").format(filename), exc_info=True, parent=self) log.error("PermissionError when trying to write report.", exc_info=True) def is_empty(self): return not self.table_model.rowCount() def is_changed(self): return self.report_changed @staticmethod def set_instance(report): app_inst = QApplication.instance() app_inst._report_window = report @staticmethod def get_instance(): app_inst = QApplication.instance() if not hasattr(app_inst, "_report_window"): report = OWReport() app_inst._report_window = report return app_inst._report_window @staticmethod def get_canvas_instance(): for widget in QApplication.topLevelWidgets(): if isinstance(widget, CanvasMainWindow): return widget
class OWReport(OWWidget): name = "Report" save_dir = Setting("") open_dir = Setting("") def __init__(self): super().__init__() self._setup_ui_() self.report_changed = False index_file = pkg_resources.resource_filename(__name__, "index.html") with open(index_file, "r") as f: self.report_html_template = f.read() def _setup_ui_(self): self.table_model = ReportItemModel(0, len(Column.__members__)) self.table = ReportTable(self.controlArea) self.table.setModel(self.table_model) self.table.setShowGrid(False) self.table.setSelectionBehavior(QTableView.SelectRows) self.table.setSelectionMode(QTableView.SingleSelection) self.table.setWordWrap(False) self.table.setMouseTracking(True) self.table.verticalHeader().setSectionResizeMode(QHeaderView.Fixed) self.table.verticalHeader().setDefaultSectionSize(20) self.table.verticalHeader().setVisible(False) self.table.horizontalHeader().setVisible(False) self.table.setFixedWidth(250) self.table.setColumnWidth(Column.item, 200) self.table.setColumnWidth(Column.remove, 23) self.table.setColumnWidth(Column.scheme, 25) self.table.clicked.connect(self._table_clicked) self.table.selectionModel().selectionChanged.connect( self._table_selection_changed) self.controlArea.layout().addWidget(self.table) self.last_scheme = None self.scheme_button = gui.button( self.controlArea, self, "Back to Last Scheme", callback=self._show_last_scheme ) box = gui.hBox(self.controlArea) box.setContentsMargins(-6, 0, -6, 0) self.save_button = gui.button( box, self, "Save", callback=self.save_report, disabled=True ) self.print_button = gui.button( box, self, "Print", callback=self._print_report, disabled=True ) class PyBridge(QObject): @pyqtSlot(str) def _select_item(myself, item_id): item = self.table_model.get_item_by_id(item_id) self.table.selectRow(self.table_model.indexFromItem(item).row()) self._change_selected_item(item) @pyqtSlot(str, str) def _add_comment(myself, item_id, value): item = self.table_model.get_item_by_id(item_id) item.comment = value self.report_changed = True self.report_view = WebviewWidget(self.mainArea, bridge=PyBridge(self)) self.mainArea.layout().addWidget(self.report_view) @deprecated("Widgets should not be pickled") def __getstate__(self): rep_dict = self.__dict__.copy() for key in ('_OWWidget__env', 'controlArea', 'mainArea', 'report_view', 'table', 'table_model'): del rep_dict[key] items_len = self.table_model.rowCount() return rep_dict, [self.table_model.item(i) for i in range(items_len)] @deprecated("Widgets should not be pickled") def __setstate__(self, state): rep_dict, items = state self.__dict__.update(rep_dict) self._setup_ui_() for i in range(len(items)): item = items[i] self.table_model.add_item( ReportItem(item.name, item.html, item.scheme, item.module, item.icon_name, item.comment) ) def _table_clicked(self, index): if index.column() == Column.remove: self._remove_item(index.row()) indexes = self.table.selectionModel().selectedIndexes() if indexes: item = self.table_model.item(indexes[0].row()) self._scroll_to_item(item) self._change_selected_item(item) if index.column() == Column.scheme: self._show_scheme(index.row()) def _table_selection_changed(self, new_selection, _): if new_selection.indexes(): item = self.table_model.item(new_selection.indexes()[0].row()) self._scroll_to_item(item) self._change_selected_item(item) def _remove_item(self, row): self.table_model.removeRow(row) self._empty_report() self.report_changed = True self._build_html() def clear(self): self.table_model.clear() self._empty_report() self.report_changed = True self._build_html() def _add_item(self, widget): name = widget.get_widget_name_extension() name = "{} - {}".format(widget.name, name) if name else widget.name item = ReportItem(name, widget.report_html, self._get_scheme(), widget.__module__, widget.icon) self.table_model.add_item(item) self._empty_report() self.report_changed = True return item def _empty_report(self): # disable save and print if no reports self.save_button.setEnabled(self.table_model.rowCount()) self.print_button.setEnabled(self.table_model.rowCount()) def _build_html(self): html = self.report_html_template html += "<body>" for i in range(self.table_model.rowCount()): item = self.table_model.item(i) html += "<div id='{}' class='normal' " \ "onClick='pybridge._select_item(this.id)'>{}<div " \ "class='textwrapper'><textarea " \ "placeholder='Write a comment...'" \ "onInput='this.innerHTML = this.value;" \ "pybridge._add_comment(this.parentNode.parentNode.id, this.value);'" \ ">{}</textarea></div>" \ "</div>".format(item.id, item.html, item.comment) html += "</body></html>" self.report_view.setHtml(html) def _scroll_to_item(self, item): self.report_view.evalJS( "document.getElementById('{}').scrollIntoView();".format(item.id) ) def _change_selected_item(self, item): self.report_view.evalJS( "var sel_el = document.getElementsByClassName('selected')[0]; " "if (sel_el.id != {}) " " sel_el.className = 'normal';".format(item.id)) self.report_view.evalJS( "document.getElementById('{}').className = 'selected';" .format(item.id)) self.report_changed = True def make_report(self, widget): item = self._add_item(widget) self._build_html() self._scroll_to_item(item) self.table.selectRow(self.table_model.rowCount() - 1) def _get_scheme(self): canvas = self.get_canvas_instance() return canvas.get_scheme_xml() if canvas else None def _show_scheme(self, row): scheme = self.table_model.item(row).scheme canvas = self.get_canvas_instance() if canvas: document = canvas.current_document() if document.isModifiedStrict(): self.last_scheme = canvas.get_scheme_xml() self._load_scheme(scheme) def _show_last_scheme(self): if self.last_scheme: self._load_scheme(self.last_scheme) def _load_scheme(self, contents): # forcibly load the contents into the associated CanvasMainWindow # instance if one exists. Preserve `self` as the designated report. canvas = self.get_canvas_instance() if canvas is not None: document = canvas.current_document() old = document.scheme() if old.has_report() and old.report_view() is self: # remove self so it is not closed old.set_report_view(None) canvas.load_scheme_xml(contents) scheme = canvas.current_document().scheme() scheme.set_report_view(self) def save_report(self): """Save report""" formats = OrderedDict((('HTML (*.html)', '.html'), ('PDF (*.pdf)', '.pdf'), ('Report (*.report)', '.report'))) filename, selected_format = QFileDialog.getSaveFileName( self, "Save Report", self.save_dir, ';;'.join(formats.keys())) if not filename: return QDialog.Rejected # Set appropriate extension if not set by the user expect_ext = formats[selected_format] if not filename.endswith(expect_ext): filename += expect_ext self.save_dir = os.path.dirname(filename) self.saveSettings() _, extension = os.path.splitext(filename) if extension == ".pdf": printer = QPrinter() printer.setPageSize(QPrinter.A4) printer.setOutputFormat(QPrinter.PdfFormat) printer.setOutputFileName(filename) self._print_to_printer(printer) elif extension == ".report": self.save(filename) else: def save_html(contents): try: with open(filename, "w", encoding="utf-8") as f: f.write(contents) except PermissionError: self.permission_error(filename) save_html(self.report_view.html()) self.report_changed = False return QDialog.Accepted def _print_to_printer(self, printer): filename = printer.outputFileName() if filename: try: # QtWebEngine return self.report_view.page().printToPdf(filename) except AttributeError: try: # QtWebKit return self.report_view.print_(printer) except AttributeError: # QtWebEngine 5.6 pass # Fallback to printing widget as an image self.report_view.render(printer) def _print_report(self): printer = QPrinter() print_dialog = QPrintDialog(printer, self) print_dialog.setWindowTitle("Print report") if print_dialog.exec_() != QDialog.Accepted: return self._print_to_printer(printer) def save(self, filename): attributes = {} for key in ('last_scheme', 'open_dir'): attributes[key] = getattr(self, key, None) items = [self.table_model.item(i) for i in range(self.table_model.rowCount())] report = dict(__version__=1, attributes=attributes, items=items) try: with open(filename, 'wb') as f: pickle.dump(report, f) except PermissionError: self.permission_error(filename) @classmethod def load(cls, filename): with open(filename, 'rb') as f: report = pickle.load(f) if not isinstance(report, dict): return report self = cls() self.__dict__.update(report['attributes']) for item in report['items']: self.table_model.add_item( ReportItem(item.name, item.html, item.scheme, item.module, item.icon_name, item.comment) ) return self def permission_error(self, filename): message_critical( self.tr("Permission error when trying to write report."), title=self.tr("Error"), informative_text=self.tr("Permission error occurred " "while saving '{}'.").format(filename), exc_info=True, parent=self) log.error("PermissionError when trying to write report.", exc_info=True) def is_empty(self): return not self.table_model.rowCount() def is_changed(self): return self.report_changed @staticmethod def set_instance(report): warnings.warn( "OWReport.set_instance is deprecated", DeprecationWarning, stacklevel=2 ) app_inst = QApplication.instance() app_inst._report_window = report @staticmethod def get_instance(): warnings.warn( "OWReport.get_instance is deprecated", DeprecationWarning, stacklevel=2 ) app_inst = QApplication.instance() if not hasattr(app_inst, "_report_window"): report = OWReport() app_inst._report_window = report return app_inst._report_window def get_canvas_instance(self): # type: () -> Optional[CanvasMainWindow] """ Return a CanvasMainWindow instance to which this report is attached. Return None if not associated with any window. Returns ------- window : Optional[CanvasMainWindow] """ # Run up the parent/window chain parent = self.parent() if parent is not None: window = parent.window() if isinstance(window, CanvasMainWindow): return window return None def copy_to_clipboard(self): self.report_view.triggerPageAction(self.report_view.page().Copy)