Пример #1
0
class PyMdWizardMainForm(QMainWindow):

    max_recent_files = 10

    def __init__(self, parent=None):
        super(self.__class__, self).__init__()

        self.settings = QSettings("USGS", "pymdwizard")
        self.cur_fname = ""
        self.file_watcher = None

        # list of buttons for opening recently accessed files
        self.recent_file_actions = []
        # list of widgets that are currently styled as errors
        self.error_widgets = []
        # the last error widget that was highlighted
        self.last_highlight = None
        self.last_updated = None
        self.ui = None
        self.metadata_root = None
        self.build_ui()
        self.connect_events()
        self.env_cache = {}

        self.sb_file = False
        self.sb_locator = SBLocator(mainform=self)
        utils.set_window_icon(self.sb_locator)

        self.load_default()

        use_spelling = self.settings.value("use_spelling", "true")
        if isinstance(use_spelling, str):
            use_spelling = eval(use_spelling.capitalize())
        self.switch_spelling(use_spelling)

    def build_ui(self):
        """
        Build and modify this widget's GUI

        Returns
        -------
        None
        """
        self.ui = UI_MainWindow.Ui_MainWindow()
        self.ui.setupUi(self)

        utils.set_window_icon(self, remove_help=False)

        # Initial window size/pos last saved. Use default values for first time
        self.resize(self.settings.value("size", QSize(1300, 700)))
        self.move(self.settings.value("pos", QPoint(50, 50)))

        self.metadata_root = MetadataRoot()
        self.ui.centralwidget.layout().addWidget(self.metadata_root)

        for i in range(PyMdWizardMainForm.max_recent_files):
            self.recent_file_actions.append(
                QAction(self, visible=False, triggered=self.open_recent_file)
            )
            self.ui.menuRecent_Files.addAction(self.recent_file_actions[i])
        self.update_recent_file_actions()

        template_fname = self.settings.value("template_fname")

        if template_fname is not None:
            just_fname = os.path.split(template_fname)[-1]
            self.ui.actionCurrentTemplate.setText("Current: " + just_fname)

        if docx is None:
            self.ui.generate_review.setEnabled(False)

        self.setAcceptDrops(True)

        self.error_list = ErrorList(main_form=self)
        self.error_list_dialog = QDialog(self)
        self.error_list_dialog.setWindowTitle("FGDC Validation Errors")
        self.error_list_dialog.setLayout(self.error_list.layout())
        self.error_list_dialog.resize(600, 400)

    def connect_events(self):
        """
        Connect the appropriate GUI components with the corresponding functions

        Returns
        -------
        None
        """
        self.ui.actionOpen.triggered.connect(self.open_file)
        self.ui.actionSave.triggered.connect(self.save_file)
        self.ui.actionSave_as.triggered.connect(self.save_as)
        self.ui.actionExit.triggered.connect(self.exit)
        self.ui.actionRun_Validation.triggered.connect(self.validate)
        self.ui.actionClear_validation.triggered.connect(self.clear_validation)
        self.ui.actionPreview.triggered.connect(self.preview)
        self.ui.actionNew.triggered.connect(self.new_record)
        self.ui.actionSettings.triggered.connect(self.set_settings)
        self.ui.actionLaunch_Jupyter.triggered.connect(self.launch_jupyter)
        self.ui.generate_review.triggered.connect(self.generate_review_doc)
        self.ui.actionLaunch_Help.triggered.connect(self.launch_help)
        self.ui.actionCheck_for_Updates.triggered.connect(self.check_for_updates)
        self.ui.actionAbout.triggered.connect(self.about)
        self.ui.actionData_Quality.triggered.connect(self.use_dataqual)
        self.ui.actionSpatial.triggered.connect(self.use_spatial)
        self.ui.actionEntity_and_Attribute.triggered.connect(self.use_eainfo)
        self.ui.actionDistribution.triggered.connect(self.use_distinfo)
        self.ui.actionSpelling_flag.triggered.connect(self.spelling_switch_triggered)
        self.ui.anacondaprompt.triggered.connect(self.anacondaprompt)
        self.ui.actionOpen_sb.triggered.connect(self.open_sb_file)

    def anacondaprompt(self):

        if os.name == "nt":
            root_dir = utils.get_install_dname("root")
            my_env = os.environ.copy()
            my_env["PYTHONPATH"] = os.path.join(root_dir, "Python36_64")
            my_env["PATH"] = ";".join(
                [
                    os.path.join(root_dir, "Python36_64", "Scripts", "conda_exes"),
                    my_env["PATH"],
                ]
            )

            pydir = utils.get_install_dname("python")
            my_env["PATH"] = ";".join(
                [os.path.join(pydir, "Scripts", "conda_exes"), my_env["PATH"]]
            )
            self.ui.actionOpen_sb.triggered.connect(self.open_sb_file)
            activatebat = os.path.join(pydir, "Scripts", "conda_exe", "activate.bat")

            msg = "This is experimental functionality used for opening an Anaconda command prompt set to"
            msg += "\nthe Python environment shipped with the MetadataWizard.\n\n"
            msg += "The base conda env in this prompt is the one to use, so do not use the activate command."
            msg += "\nUse: conda install ...package.. to install new packages into the MetadataWizard envronment."
            QMessageBox.information(self, "Conda instructions", msg)

            subprocess.Popen(
                ["start", "cmd", activatebat, pydir], env=my_env, cwd=pydir, shell=True
            )
        else:
            msg = "This experimental functionality not yet implemented for Mac or Linux builds"
            QMessageBox.warning(self, "Not implemented", msg)

    def open_recent_file(self):
        """
        handles the opening of a recent file selection
        Returns
        -------
        None
        """
        action = self.sender()
        if action:
            self.load_file(action.data())
            self.set_current_file(action.data())

    def get_xml_fname(self):
        """

        Returns
        -------
        str: path and filename of the selected file or
        empty string if none was selected
        """
        recent_files = self.settings.value("recentFileList", [])
        if recent_files:
            dname, fname = os.path.split(recent_files[0])
        else:
            fname, dname = "", ""

        fname = QFileDialog.getOpenFileName(
            self, fname, dname, filter="XML Files (*.xml)"
        )

        if fname[0]:
            return fname[0]
        else:
            return ""

    def open_file(self, fname=None):
        """
        Browse to a file and load it if it is acceptable

        Returns
        -------
        None
        """
        self.sb_file = False
        if fname is None or not fname:
            fname = self.get_xml_fname()

        if fname:
            self.load_file(fname)
            self.set_current_file(fname)
            self.update_recent_file_actions()

    def open_sb_file(self, hash=None):
        """
        download a
        Parameters
        ----------
        hash : str
            Tag of item to be edited on SB

        Returns
        -------
        None
        """
        self.sb_locator.show()

    def load_file(self, fname, check_for_changes=True):
        """
        load a file's content into the application.

        Parameters
        ----------
        fname : str
                full file path and name of the file to load
        check_for_changes : bool
                flag to check for save prompt if file has unsaved changes
        Returns
        -------
        None
        """

        if check_for_changes:
            changed = self.check_for_changes()
            if changed == "Cancel":
                return changed

        self.file_watcher = QFileSystemWatcher([fname])
        self.file_watcher.fileChanged.connect(self.file_updated)
        self.last_updated = time.time()

        self.clear_validation()

        # check that we have read write access to the file
        file = QFile(fname)
        if not file.open(QFile.ReadOnly | QFile.Text):
            msg = "Cannot read file %s:\n%s." % (fname, file.errorString())
            QMessageBox.warning(self, "Recent Files", msg)
            return
        file.close()

        self.load_file_content(fname)

    def load_file_content(self, fname):
        QApplication.setOverrideCursor(Qt.WaitCursor)
        QApplication.processEvents()
        self.metadata_root.clear_widget()
        self.ui.actionData_Quality.setChecked(True)
        self.use_dataqual(True)
        self.ui.actionSpatial.setChecked(True)
        self.use_spatial(True)
        self.ui.actionEntity_and_Attribute.setChecked(True)
        self.use_eainfo(True)
        self.ui.actionDistribution.setChecked(True)
        self.use_distinfo(True)
        try:
            new_record = xml_utils.fname_to_node(fname)
            self.metadata_root.from_xml(new_record)
            self.statusBar().showMessage("File loaded", 10000)
        except BaseException as e:
            import traceback

            msg = "Cannot open file %s:\n%s." % (fname, traceback.format_exc())
            QApplication.restoreOverrideCursor()
            QMessageBox.warning(self, "Recent Files", msg)
        QApplication.restoreOverrideCursor()

    def file_updated(self):
        """
        The function that fires when the file watcher detects that the
        current file has changed on the file system.  Prompts the user and
        loads the new file if they choose to.

        Returns
        -------
        None
        """
        if time.time() - self.last_updated > 4:
            msg = (
                "The file you are editing has been changed on disk.  "
                "Would you like to reload this File?"
            )
            alert = QDialog()
            self.last_updated = time.time()
            confirm = QMessageBox.question(
                self, "File Changed", msg, QMessageBox.Yes | QMessageBox.No
            )
            if confirm == QMessageBox.Yes:
                self.load_file(self.cur_fname)

    def save_as(self):
        """
        Navigate to a new or existing file and save the current document
        into this file.

        Returns
        -------
        None
        """
        fname = self.get_save_name()
        if fname:
            self.set_current_file(fname)
            self.update_recent_file_actions()
            self.save_file()

    def get_save_name(self):
        """
        launches a saveas dialog to browse to a file to save to.
        starts in the directory of the most recently sopened file

        Returns
        -------
        str: file name and path
        """
        recent_files = self.settings.value("recentFileList", [])
        if recent_files:
            dname, fname = os.path.split(recent_files[0])
        else:
            fname, dname = "", ""

        fname = QFileDialog.getSaveFileName(
            self, "Save As", dname, filter="XML Files (*.xml)"
        )

        return fname[0]

    def save_file(self):
        """
        Save the current xml document.  Prompts for a filename if one
        has not been set yet.

        Returns
        -------
        None
        """
        if not self.cur_fname:
            fname = self.get_save_name()
            if not fname:
                return
        else:
            fname = self.cur_fname

        fname_msg = utils.check_fname(fname)
        if not fname_msg == "good":
            msg = "Cannot write to :\n  {}.".format(fname)
            QMessageBox.warning(self, "Metadata Wizard", msg)
            return

        tool_comment = (
            "Record created using version {} of the "
            "USGS Metadata Wizard tool. (https://github.com/usgs/"
            "fort-pymdwizard)".format(__version__)
        )
        xml_contents = self.metadata_root.to_xml()
        comment = xml_utils.xml_node(tag="", text=tool_comment, index=0, comment=True)
        xml_contents.addprevious(comment)
        xml_utils.save_to_file(xml_contents, fname)
        self.last_updated = time.time()

        self.set_current_file(fname)
        self.statusBar().showMessage("File saved", 2000)

        if self.sb_file:
            self.sb_locator.put_fgdc_file()

    def new_record(self):
        """
        Create a new record.
        Starts by making a copy of the template file 'CSDGM_Template.xml'.
            in the resources folder to a name selected in a save as dialog.
        Then updates the MD date to today.
        Returns
        -------
        None
        """
        self.load_default()
        save_as_fname = self.get_save_name()
        if save_as_fname:
            template_fname = self.settings.value("template_fname")
            if template_fname is None or not os.path.exists(template_fname):
                template_fname = utils.get_resource_path("CSDGM_Template.xml")

            shutil.copyfile(template_fname, save_as_fname)
            self.load_file(save_as_fname)
            self.set_current_file(save_as_fname)
            self.update_recent_file_actions()

            today = fgdc_utils.format_date(datetime.datetime.now())
            self.metadata_root.metainfo.metd.set_date(today)
            this_year = today[:4]
            self.metadata_root.idinfo.citation.ui.pubdate_widget.set_date(this_year)

    def set_settings(self):
        self.settings_dialog = Settings(mainform=self)
        self.settings_dialog.setWindowTitle("MetadataWizard Settings")
        utils.set_window_icon(self.settings_dialog)
        self.settings_dialog.show()

    def load_default(self):
        template_fname = self.settings.value("template_fname")

        if template_fname is None:
            template_fname = utils.get_resource_path("CSDGM_Template.xml")
        elif not os.path.exists(template_fname):
            msg = (
                "The previous template file specified, {}, could not be "
                "found.".format(template_fname)
            )
            msg += "\nCheck that the file has not beed deleted, renamed " "or moved."
            msg += "Defaulting to the built in template.".format(template_fname)
            QMessageBox.warning(self, "Template file missing", msg)
            template_fname = utils.get_resource_path("CSDGM_Template.xml")

        self.load_file_content(template_fname)
        self.cur_fname = ""

        today = fgdc_utils.format_date(datetime.datetime.now())
        self.metadata_root.metainfo.metd.set_date(today)

    def set_current_file(self, fname):
        """
        The procedure for storing and displaying a new current file
        The following get done:
        1 - Display the file name without path in the apps title bar
        2 - Insert the file name into the top slot of the recent files
        3 - Save this list out to the setting variable

        Parameters
        ----------
        fname : str
            The file name and path that will be used

        Returns
        -------
        None
        """
        self.cur_fname = fname
        if fname:
            stripped_name = QFileInfo(fname).fileName()
            title = "Metadata Wizard - {}".format(stripped_name)
            self.setWindowTitle(title)

            files = self.settings.value("recentFileList", [])

            try:
                files.remove(fname)
            except ValueError:
                pass

            files.insert(0, fname)
            del files[PyMdWizardMainForm.max_recent_files :]

            self.settings.setValue("recentFileList", files)

            for widget in QApplication.topLevelWidgets():
                if isinstance(widget, PyMdWizardMainForm):
                    widget.update_recent_file_actions()
        else:
            self.setWindowTitle("Metadata Wizard")

    def update_recent_file_actions(self):
        """
        Update the actions (menu items) in the recent files list to
        reflect the recent file paths stored in the 'recentFileList' setting

        Returns
        -------
        None
        """
        files = self.settings.value("recentFileList", [])

        num_recent_files = min(len(files), PyMdWizardMainForm.max_recent_files)

        for i in range(num_recent_files):
            stripped_name = QFileInfo(files[i]).fileName()
            text = "&%d %s" % (i + 1, stripped_name)
            self.recent_file_actions[i].setText(text)
            self.recent_file_actions[i].setData(files[i])
            self.recent_file_actions[i].setVisible(True)

        for j in range(num_recent_files, PyMdWizardMainForm.max_recent_files):
            self.recent_file_actions[j].setVisible(False)

    def check_for_changes(self):
        try:
            if self.cur_fname and os.path.exists(self.cur_fname):
                cur_xml = xml_utils.node_to_string(self.metadata_root.to_xml())
                disk_xml = xml_utils.node_to_string(
                    xml_utils.fname_to_node(self.cur_fname)
                )

            if cur_xml != disk_xml:
                msg = "Do you want to save your changes?"
                alert = QDialog()
                self.last_updated = time.time()
                confirm = QMessageBox.question(
                    self,
                    "Save Changes",
                    msg,
                    QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel,
                )
                if confirm == QMessageBox.Yes:
                    xml_utils.save_to_file(self.metadata_root.to_xml(), self.cur_fname)
                elif confirm == QMessageBox.Cancel:
                    return "Cancel"
        except:
            pass
        return None

    def exit(self):
        """
        Before exiting check if the current contents match what is on the
        file system.  If the do not match ask user if they would like to
        save or cancel the exit

        Returns
        -------
        str :
        'Close' or 'Cancel' depending on user choice.
        """
        changed = self.check_for_changes()
        if changed == "Cancel":
            return changed
        else:
            self.close()
            return "Close"

    def closeEvent(self, event):
        """ Intercept the builtin closeEvent so that we can check for
        changes and ask if we should change.

        Parameters
        ----------
        event

        Returns
        -------

        """
        if self.exit() == "Close":
            self.settings.setValue("size", self.size())
            self.settings.setValue("pos", self.pos())
            event.accept()
        else:
            event.ignore()

    def use_dataqual(self, sender=None):
        self.metadata_root.use_section("dataqual", sender)

    def use_spatial(self, sender=None):
        self.metadata_root.use_section("spatial", sender)

    def use_eainfo(self, sender=None):
        self.metadata_root.use_section("eainfo", sender)

    def use_distinfo(self, sender=None):
        self.metadata_root.use_section("distinfo", sender)

    def clear_validation(self):
        """
        Remove the error highlighting from all the error widgets

        Returns
        -------
        None
        """
        annotation_lookup = fgdc_utils.get_fgdc_lookup()

        for widget in self.error_widgets:
            if not sip.isdeleted(widget) and widget.objectName() not in [
                "metadata_root",
                "fgdc_metadata",
            ]:
                widget.setStyleSheet("""""")
                print(widget.objectName())
                shortname = widget.objectName().replace("fgdc_", "")
                if shortname[-1].isdigit():
                    shortname = shortname[:-1]
                try:
                    widget.setToolTip(annotation_lookup[shortname]["annotation"])
                except KeyError:
                    widget.setToolTip("")

        self.error_widgets = []
        self.error_list.clear_errors()
        self.error_list_dialog.hide()

    def validate(self):
        """
        Check the current record against the schema and highlight any
        error widgets

        Returns
        -------
        None
        """

        self.error_list_dialog.show()
        if self.metadata_root.schema == "bdp":
            xsl_fname = utils.get_resource_path(
                "FGDC/BDPfgdc-std-001-1998-annotated.xsd"
            )
        else:
            xsl_fname = utils.get_resource_path("FGDC/fgdc-std-001-1998-annotated.xsd")
        from pymdwizard.core import fgdc_utils

        errors = fgdc_utils.validate_xml(self.metadata_root.to_xml(), xsl_fname)

        self.clear_validation()

        marked_errors = []

        # We need to expand / populate all attributes that have an error
        for error in errors:
            try:
                xpath, error_msg, line_num = error
                if "attr" in xpath:
                    try:
                        detailed_index = xpath.split("/detailed[")[1].split("/")[0][:-1]
                        detailed_index = int(detailed_index) - 1
                    except IndexError:
                        detailed_index = 0

                    try:
                        attr_index = xpath.split("/attr[")[1].split("/")[0][:-1]
                        attr_index = int(attr_index) - 1
                    except IndexError:
                        attr_index = 0

                    self.metadata_root.eainfo.detaileds[
                        detailed_index
                    ].attributes.attrs[attr_index].regular_me()
                    self.metadata_root.eainfo.detaileds[
                        detailed_index
                    ].attributes.attrs[attr_index].supersize_me()
            except:
                pass

        widget_lookup = self.metadata_root.make_tree(widget=self.metadata_root)
        self.metadata_root.add_children(
            self.metadata_root.spatial_tab, widget_lookup.metadata.idinfo
        )
        self.metadata_root.add_children(
            self.metadata_root.dataqual.sourceinput,
            widget_lookup.metadata.dataqual.lineage,
        )
        error_count = 0
        for error in errors:

            try:
                xpath, error_msg, line_num = error
                if xpath not in marked_errors:
                    self.error_list.add_error(error_msg, xpath)
                    marked_errors.append(xpath)

                    # widget = self.metadata_root.get_widget(xpath)
                    widgets = widget_lookup.xpath_march(xpath, as_list=True)
                    for widget in widgets:
                        if isinstance(widget, list):
                            for w in widget:
                                print("problem highlighting error", xpath, widget)
                        else:
                            self.highlight_error(widget.widget, error_msg)
                            self.error_widgets.append(widget.widget)
                            error_count += 1
            except BaseException as e:
                import traceback

                msg = "Error encountered highlighting error:"
                msg += "\t" + xpath
                msg += "\n\n" + traceback.format_exc()
                QMessageBox.warning(self, "Bug encountered", msg)
        widget_lookup = self.metadata_root.make_tree(widget=self.metadata_root)
        if errors:
            msg = "There are {} errors in this record".format(error_count)
            self.statusBar().showMessage(msg, 20000)
            msg += "\n\n These errors are highlighted in red in the form below."
            msg += "\n\n These errors are also listed in the Validation Errors Form that just popped up."
            msg += "\n Clicking each error will take you to the section it is contained in."
            msg += "\n Note that some highlighed errors can be in collapsed items, scrolled out of view, or in non-selected tabs"
            QMessageBox.warning(self, "Validation", msg)
            self.error_list_dialog.show()
        else:
            msg = "Congratulations there were No FGDC Errors!"
            self.statusBar().showMessage(msg, 20000)
            QMessageBox.information(self, "Validation", msg)

    def goto_error(self, sender):
        """
        super highlight the selected error and switch the tab to the section
        that contains this error.

        Parameters
        ----------
        sender : QWidget

        Returns
        -------
        None
        """

        xpath = sender.data(1)
        section = xpath.split("/")[1]

        if section == "idinfo":
            subsection = xpath.split("/")[2]
            if subsection == "spdom":
                parent_section = self.metadata_root.switch_section(2)
            else:
                parent_section = self.metadata_root.switch_section(0)
        elif section == "dataqual":
            parent_section = self.metadata_root.switch_section(1)
        elif section == "spdoinfo" or section == "spref":
            parent_section = self.metadata_root.switch_section(2)
        elif section == "eainfo":
            parent_section = self.metadata_root.switch_section(3)
        elif section == "eainfo":
            parent_section = self.metadata_root.switch_section(3)
        elif section == "distinfo":
            parent_section = self.metadata_root.switch_section(4)
        elif section == "metainfo":
            parent_section = self.metadata_root.switch_section(5)

        if self.last_highlight is not None and not sip.isdeleted(self.last_highlight):
            self.highlight_error(self.last_highlight, self.last_highlight.toolTip())

        widget_lookup = self.metadata_root.make_tree(widget=self.metadata_root)
        bad_widget = widget_lookup.xpath_march(xpath, as_list=True)

        try:
            parent_wizwidget = [
                thing
                for thing in parent_section.children()
                if isinstance(thing, WizardWidget)
            ][0]
            parent_wizwidget.scroll_area.ensureWidgetVisible(bad_widget[0].widget)
        except:
            pass

        self.last_highlight = bad_widget[0].widget
        self.highlight_error(bad_widget[0].widget, sender.text(), superhot=True)

    def highlight_error(self, widget, error_msg, superhot=False):
        """
        Highlight the given widget and set it's tooltip msg to error_msg

        Parameters
        ----------
        widget : QWidget
        error_msg : str
                    the message that will appear in the tooltip
        superhot : bool
            whether to use the regular highlight
            or also include a black thick outline

        Returns
        -------
            None
        """

        if widget.objectName() in [
            "fgdc_attr",
            "fgdc_edomv",
            "fgdc_edomvd",
            "fgdc_edomvds",
            "fgdc_attrlabl",
            "fgdc_attrdef",
            "fgdc_attrdefs",
            "fgdc_codesetd",
            "fgdc_edom",
            "fgdc_rdom",
            "fgdc_udom",
            "fgdc_rdommin",
            "fgdc_rdommax",
            "fgdc_codesetn",
            "fgdc_codesets",
            "fgdc_attrdomv",
        ]:
            self.highlight_attr(widget)

        if widget.objectName() in [
            "fgdc_themekey",
            "fgdc_themekt",
            "fgdc_placekey",
            "fgdc_placekt",
            "fgdc_procdesc",
            "fgdc_srcused",
            "fgdc_srcprod",
        ]:
            self.highlight_tab(widget)

        if superhot:
            color = "rgb(223,1,74)"
            lw = "border: 3px solid black;"
        else:
            color = "rgb(223,1,74)"
            lw = ""

        color = "rgb(225,67,94)"

        if widget.objectName() not in ["metadata_root", "fgdc_metadata"]:
            try:
                widget.setToolTip(error_msg)
                widget.setStyleSheet(
                    """
                QGroupBox#{widgetname}{{
                background-color: {color};
                border: 2px solid red;
                subcontrol-position: top left; /* position at the top left*/
                padding-top: 20px;
                font: bold 14px;
                color: rgb(90, 90, 90);
                }}
                QGroupBox#{widgetname}::title {{
                text-align: left;
                subcontrol-origin: padding;
                subcontrol-position: top left; /* position at the top center */padding: 3 3px;
                }}
                QLabel{{
                font: 9pt "Arial";
                color: rgb(90, 90, 90);
                }}
                QLineEdit#{widgetname}, QPlainTextEdit#{widgetname}, QComboBox#{widgetname} {{
                font: 9pt "Arial";
                color: rgb(50, 50, 50);
                background-color: {color};
                opacity: 25;
                {lw}
                }}
                QToolTip {{
                background-color: rgb(255,76,77);
                border-color: red;
                opacity: 255;
                }}
                """.format(
                        widgetname=widget.objectName(), color=color, lw=lw
                    )
                )
            except:
                pass

    def highlight_attr(self, widget):
        widget_parent = widget
        attr_frame = widget

        while not widget_parent.objectName() == "fgdc_attr":
            widget_parent = widget_parent.parent()
            attr_frame = widget_parent
        self.error_widgets.append(attr_frame)
        widget_parent = widget_parent.parent()

        widget_parent.supersize_me()
        error_msg = "'Validation error in hidden contents, click to show'"
        widget_parent.setToolTip(error_msg)
        widget_parent.setStyleSheet(
            """
    QFrame#{widgetname}{{
    border: 2px solid red;
    }}
        """.format(
                widgetname=attr_frame.objectName()
            )
        )

        self.error_widgets.append(widget_parent)

    def highlight_tab(self, widget):

        widget_parent = widget.parent()
        while not type(widget_parent) == QTabWidget:
            widget_parent = widget_parent.parent()

        error_msg = "'Validation error in hidden contents, click to show'"
        widget_parent.setToolTip(error_msg)
        widget_parent.setStyleSheet(
            """
    QTabBar {{
    background-color: rgb(225,67,94);
    qproperty-drawBase:0;

}}
        """
        )

        self.error_widgets.append(widget_parent)

    def spelling_switch_triggered(self, e):
        spelling_action_text = self.ui.actionSpelling_flag.text()
        use_spelling = spelling_action_text == "Turn Spelling OFF"
        self.switch_spelling(not use_spelling)

    def switch_spelling(self, use_spelling):
        """
        Handle click event of the Turn Spelling (OFF | ON) action
        Changes the action's label and updates the widget's highlighter.

        Parameters
        ----------
        e : Qt event, not used

        Returns
        -------
        None
        """
        if use_spelling:
            self.ui.actionSpelling_flag.setText("Turn Spelling OFF")
        else:
            self.ui.actionSpelling_flag.setText("Turn Spelling ON")

        self.recursive_spell(self.metadata_root, use_spelling)

        self.settings.setValue("use_spelling", use_spelling)

    def recursive_spell(self, widget, which):
        """
        Turn on or off the spelling highlighter for this widget and
        iterate through the widget's child widgets to recursively do the
        same for them.

        Parameters
        ----------
        widget : Qwidget
        which : bool
                flag to turn spelling on or off
                True = turn spelling highlighting on
                False = turn spelling highlighting off

        Returns
        -------
            None
        """
        try:
            widget.highlighter.enabled = which
            widget.highlighter.rehighlight()
        except:
            pass

        for child_widget in self.metadata_root.get_children(widget):
            self.recursive_spell(child_widget, which)

    def dragEnterEvent(self, e):
        if e.mimeData().hasUrls:
            e.accept()
        else:
            e.ignore()

    def dragMoveEvent(self, e):
        if e.mimeData().hasUrls() and e.mimeData().urls()[0].isLocalFile():
            e.accept()
        else:
            e.ignore()

    def dropEvent(self, e):
        """
        Drop files directly onto the widget
        File locations are stored in fname
        :param e:
        :return:
        """
        try:
            if e.mimeData().hasUrls:
                e.setDropAction(Qt.CopyAction)

                url = e.mimeData().urls()[0]
                fname = url.toLocalFile()
                if os.path.isfile(fname):
                    self.open_file(fname)
                e.accept()
            else:
                e.ignore()
        except:
            # if anything goes wrong at all, pass silently.
            # This is just a convenience function
            pass

    def preview(self):
        """
        Shows a preview window with the xml content rendered using stylesheet

        Returns
        -------
        None
        """

        xsl_fname = utils.get_resource_path("FGDC/FGDC_Stylesheet.xsl")
        transform = xml_utils.load_xslt(xsl_fname)
        result = transform(self.metadata_root.to_xml())

        tmp = tempfile.NamedTemporaryFile(suffix=".html")
        tmp.close()
        result.write(tmp.name)

        self.preview = Preview(url=tmp.name)

        self.preview_dialog = QDialog(self)
        self.preview_dialog.setWindowTitle("Metadata Preview")
        self.preview_dialog.setLayout(self.preview.layout())

        self.preview_dialog.exec_()

    def launch_help(self):
        root_dname = utils.get_install_dname("pymdwizard")

        help_html = os.path.join(root_dname, "docs", "html_output", "index.html")

        if not os.path.exists(help_html):
            gui_fname = os.path.dirname(os.path.realpath(__file__))
            help_html = os.path.join(
                gui_fname, "..", "..", "docs", "html_output", "index.html"
            )

        self.preview = Preview(url=help_html)

        self.preview_dialog = QDialog(self)
        self.preview_dialog.setWindowTitle("MetadataWizard Help")
        self.preview_dialog.setLayout(self.preview.layout())

        self.preview_dialog.exec_()

    def generate_review_doc(self):
        if self.cur_fname:
            out_fname = self.cur_fname[:-4] + "_REVIEW.docx"

            if self.metadata_root.schema == "bdp":
                which = "bdp"
            else:
                which = "fgdc"

            if time.time() - self.last_updated > 4:
                msg = "Would you like to save the current file before continuing?"
                alert = QDialog()
                self.last_updated = time.time()
                confirm = QMessageBox.question(
                    self,
                    "File save",
                    msg,
                    QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel,
                )
                if confirm == QMessageBox.Yes:
                    self.save_file()
                elif confirm == QMessageBox.Cancel:
                    return
            try:
                cur_content = xml_utils.XMLRecord(self.cur_fname)
                review_utils.generate_review_report(cur_content, out_fname, which=which)

                import os, sys, subprocess

                def open_file(filename):
                    if sys.platform == "win32":
                        os.startfile('"{}"'.format(filename))
                    elif sys.platform == "darwin":
                        opener = "open"
                        subprocess.call([opener, filename])

                open_file(out_fname)

                msg = "Review document available at: {}".format(out_fname)
                msg += "\n\nReview document now opening in default application..."
                QMessageBox.information(self, "Review finished", msg)
            except BaseException:
                import traceback

                msg = "Problem encountered generating review document:\n{}".format(
                    traceback.format_exc()
                )
                QMessageBox.warning(self, "Problem encountered", msg)

    def launch_jupyter(self):
        """
        Launches a jupyter notebook server in our examples directory

        Returns
        -------
        None
        """
        last_kernel = self.settings.value("last_kernel", "")
        jupyter_dnames = self.settings.value("jupyter_dnames", [])
        if not jupyter_dnames:
            install_dir = utils.get_install_dname()
            jupyter_dnames = [os.path.join(install_dir, "examples")]
            self.settings.setValue("jupyter_dnames", jupyter_dnames)

        self.jupyter_dialog = JupyterStarter(
            last_kernel=last_kernel,
            previous_dnames=jupyter_dnames,
            update_function=self.update_jupyter_dnames,
        )
        utils.set_window_icon(self.jupyter_dialog)
        self.jupyter_dialog.show()

    def update_jupyter_dnames(self, kernel, dname):
        jupyter_dnames = self.settings.value("jupyter_dnames", [])

        try:
            jupyter_dnames.remove(dname)
        except ValueError:
            pass
            my_env = os.environ.copy()
            # my_env["PYTHONPATH"] = os.path.join(root_dir, "Python36_64")

        jupyter_dnames.insert(0, dname)
        del jupyter_dnames[PyMdWizardMainForm.max_recent_files :]
        self.settings.setValue("jupyter_dnames", jupyter_dnames)

        self.settings.setValue("last_kernel", kernel)

    def about(self):
        """
        Display an 'about' message box with contanct info and current
        version number

        Returns
        -------
        None
        """
        msg = "The MetadataWizard was developed by the data management team <br> at the USGS Fort Collins Science Center,<br>"
        msg += "with support from the USGS Science Analytics and Synthesis (SAS), "
        msg += "and the USGS Council for Data integration (CDI).<br><br>"
        msg += "Ongoing support provided by the USGS Science Analytics and Synthesis (SAS)<br>"
        msg += f"<br><br>Version: {__version__}<br>"
        msg += "<br> Project page: <a href='https://github.com/usgs/fort-pymdwizard'>https://github.com/usgs/fort-pymdwizard</a>"
        msg += "<br><br>Contact: Colin Talbert at [email protected]"

        msgbox = QMessageBox.about(self, "About", msg)

    def check_for_updates(self, e=None, show_uptodate_msg=True):
        """
        Check if the usgs_root repo is at the same commit as this installation

        Parameters
        ----------
        e : qt event
            
        show_uptodate_msg : bool
           Whether to display a msg if no updates found

        Returns
        -------
        None
        """
        try:
            from git import Repo

            install_dir = utils.get_install_dname("pymdwizard")
            repo = Repo(install_dir)
            fetch = [r for r in repo.remotes if r.name == "usgs_root"][0].fetch()
            master = [f for f in fetch if f.name == "usgs_root/master"][0]

            if repo.head.commit != master.commit:
                msg = "An update(s) are available for the Metadata Wizard.\n"
                msg += "Would you like to install these now?"

                confirm = QMessageBox.question(
                    self, "Updates Available", msg, QMessageBox.Yes | QMessageBox.No
                )
                if confirm == QMessageBox.Yes:
                    self.update_from_github()
            elif show_uptodate_msg:
                msg = "MetadataWizard already up to date."
                QMessageBox.information(self, "No Update Needed", msg)

        except BaseException as e:
            if show_uptodate_msg:
                msg = "Problem Encountered Updating from GitHub\n\nError Message:\n"
                msg += str(e)
                QMessageBox.information(self, "Update results", msg)

    def update_from_github(self):
        """
        Merge the latest version of the Wizard into the local repo

        Returns
        -------
        None
        """

        try:
            from git import Repo

            install_dir = utils.get_install_dname("pymdwizard")
            repo = Repo(install_dir)
            fetch = [r for r in repo.remotes if r.name == "usgs_root"][0].fetch()
            master = [f for f in fetch if f.name == "usgs_root/master"][0]

            merge_msg = repo.git.merge(master.name)

            msg = "Updated Successfully from GitHub."
            QMessageBox.information(self, "Update results", msg)
        except BaseException as e:
            msg = (
                "Problem Encountered Updating from GitHub\n\n"
                "Please upgrade to the latest release by reinstalling the "
                "application from GitHub "
                "\n(https://github.com/usgs/fort-pymdwizard/releases)\n\n"
                "Error Message:\n"
            )
            msg += str(e)
            QMessageBox.information(self, "Update results", msg)

        QApplication.restoreOverrideCursor()
Пример #2
0
class PyMdWizardMainForm(QMainWindow):

    max_recent_files = 10

    def __init__(self, parent=None):
        super(self.__class__, self).__init__()

        self.cur_fname = ''
        self.file_watcher = None

        # list of buttons for opening recently accessed files
        self.recent_file_actions = []
        # list of widgets that are currently styled as errors
        self.error_widgets = []
        # the last error widget that was highlighted
        self.last_highlight = None
        self.last_updated = None
        self.ui = None
        self.metadata_root = None
        self.build_ui()
        self.connect_events()

        self.load_default()

    def build_ui(self):
        """
        Build and modify this widget's GUI

        Returns
        -------
        None
        """
        self.ui = UI_MainWindow.Ui_MainWindow()
        self.ui.setupUi(self)

        utils.set_window_icon(self)

        self.metadata_root = MetadataRoot()
        self.ui.centralwidget.layout().addWidget(self.metadata_root)

        for i in range(PyMdWizardMainForm.max_recent_files):
            self.recent_file_actions.append(
                QAction(self, visible=False, triggered=self.open_recent_file))
            self.ui.menuRecent_Files.addAction(self.recent_file_actions[i])
        self.update_recent_file_actions()

        settings = QSettings('USGS', 'pymdwizard')
        template_fname = settings.value('template_fname')

        if template_fname is not None:
            just_fname = os.path.split(template_fname)[-1]
            self.ui.actionCurrentTemplate.setText('Current: ' + just_fname)

        if docx is None:
            self.ui.generate_review.setEnabled(False)

        self.setAcceptDrops(True)

        self.error_list = ErrorList(main_form=self)
        self.error_list_dialog = QDialog(self)
        self.error_list_dialog.setWindowTitle('FGDC Validation Errors')
        self.error_list_dialog.setLayout(self.error_list.layout())
        self.error_list_dialog.resize(600, 400)

    def connect_events(self):
        """
        Connect the appropriate GUI components with the corresponding functions

        Returns
        -------
        None
        """
        self.ui.actionOpen.triggered.connect(self.open_file)
        self.ui.actionSave.triggered.connect(self.save_file)
        self.ui.actionSave_as.triggered.connect(self.save_as)
        self.ui.actionExit.triggered.connect(self.exit)
        self.ui.actionRun_Validation.triggered.connect(self.validate)
        self.ui.actionClear_validation.triggered.connect(self.clear_validation)
        self.ui.actionPreview.triggered.connect(self.preview)
        self.ui.actionNew.triggered.connect(self.new_record)
        self.ui.actionBrowseTemplate.triggered.connect(self.set_template)
        self.ui.actionRestoreBuiltIn.triggered.connect(self.restore_template)
        self.ui.actionLaunch_Jupyter.triggered.connect(self.launch_jupyter)
        self.ui.generate_review.triggered.connect(self.generate_review_doc)
        self.ui.actionLaunch_Help.triggered.connect(self.launch_help)
        self.ui.actionCheck_for_Updates.triggered.connect(self.update_from_github)
        self.ui.actionAbout.triggered.connect(self.about)
        self.ui.actionData_Quality.triggered.connect(self.use_dataqual)
        self.ui.actionSpatial.triggered.connect(self.use_spatial)
        self.ui.actionEntity_and_Attribute.triggered.connect(self.use_eainfo)
        self.ui.actionDistribution.triggered.connect(self.use_distinfo)

    def open_recent_file(self):
        """
        handles the opening of a recent file selection
        Returns
        -------
        None
        """
        action = self.sender()
        if action:
            self.load_file(action.data())
            self.set_current_file(action.data())

    def get_xml_fname(self):
        """

        Returns
        -------
        str: path and filename of the selected file or
        empty string if none was selected
        """
        settings = QSettings('USGS', 'pymdwizard')
        recent_files = settings.value('recentFileList', [])
        if recent_files:
            dname, fname = os.path.split(recent_files[0])
        else:
            fname, dname = "", ""

        fname = QFileDialog.getOpenFileName(self, fname, dname, \
                                            filter="XML Files (*.xml)")

        if fname[0]:
            return fname[0]
        else:
            return ''

    def open_file(self, fname=None):
        """
        Browse to a file and load it if it is acceptable

        Returns
        -------
        None
        """
        if fname is None or not fname:
            fname = self.get_xml_fname()

        if fname:
            self.load_file(fname)
            self.set_current_file(fname)
            self.update_recent_file_actions()

    def load_file(self, fname):
        """
        load a file's content into the application.

        Parameters
        ----------
        fname : str
                full file path and name of the file to load
        Returns
        -------
        None
        """
        changed = self.check_for_changes()
        if changed == 'Cancel':
            return changed

        self.file_watcher = QFileSystemWatcher([fname])
        self.file_watcher.fileChanged.connect(self.file_updated)
        self.last_updated = time.time()

        self.clear_validation()

        # check that we have read write access to the file
        file = QFile(fname)
        if not file.open(QFile.ReadOnly | QFile.Text):
            msg = "Cannot read file %s:\n%s." % (fname, file.errorString())
            QMessageBox.warning(self, "Recent Files", msg)
            return
        file.close()

        self.load_file_content(fname)

    def load_file_content(self, fname):
        QApplication.setOverrideCursor(Qt.WaitCursor)
        QApplication.processEvents()
        self.metadata_root.clear_widget()
        try:
            new_record = xml_utils.fname_to_node(fname)
            self.metadata_root.from_xml(new_record)
            self.statusBar().showMessage("File loaded", 10000)
        except BaseException as e:
            import traceback
            msg = "Cannot open file %s:\n%s." % (fname, traceback.format_exc())
            QApplication.restoreOverrideCursor()
            QMessageBox.warning(self, "Recent Files", msg)
        QApplication.restoreOverrideCursor()

    def file_updated(self):
        """
        The function that fires when the file watcher detects that the
        current file has changed on the file system.  Prompts the user and
        loads the new file if they choose to.

        Returns
        -------
        None
        """
        if time.time() - self.last_updated > 4:
            msg = "The file you are editing has been changed on disk.  " \
                  "Would you like to reload this File?"
            alert = QDialog()
            self.last_updated = time.time()
            confirm = QMessageBox.question(self, "File Changed", msg,
                                           QMessageBox.Yes | QMessageBox.No)
            if confirm == QMessageBox.Yes:
                self.load_file(self.cur_fname)

    def save_as(self):
        """
        Navigate to a new or existing file and save the current document
        into this file.

        Returns
        -------
        None
        """
        fname = self.get_save_name()
        if fname:
            self.set_current_file(fname)
            self.update_recent_file_actions()
            self.save_file()

    def get_save_name(self):
        """
        launches a saveas dialog to browse to a file to save to.
        starts in the directory of the most recently sopened file

        Returns
        -------
        str: file name and path
        """
        settings = QSettings('USGS', 'pymdwizard')
        recent_files = settings.value('recentFileList', [])
        if recent_files:
            dname, fname = os.path.split(recent_files[0])
        else:
            fname, dname = "", ""

        fname = QFileDialog.getSaveFileName(self, "Save As", dname, \
                                            filter="XML Files (*.xml)")

        return fname[0]

    def save_file(self):
        """
        Save the current xml document.  Prompts for a filename if one
        has not been set yet.

        Returns
        -------
        None
        """
        if not self.cur_fname:
            fname = self.get_save_name()
            if not fname:
                return
        else:
            fname = self.cur_fname

        fname_msg = utils.check_fname(fname)
        if not fname_msg == 'good':
            msg = "Cannot write to :\n  {}.".format(fname)
            QMessageBox.warning(self, "Metadata Wizard", msg)
            return

        xml_utils.save_to_file(self.metadata_root.to_xml(), fname)
        self.last_updated = time.time()

        self.set_current_file(fname)
        self.statusBar().showMessage("File saved", 2000)

    def new_record(self):
        """
        Create a new record.
        Starts by making a copy of the template file 'CSDGM_Template.xml'.
            in the resources folder to a name selected in a save as dialog.
        Then updates the MD date to today.
        Returns
        -------
        None
        """
        self.load_default()
        save_as_fname = self.get_save_name()
        if save_as_fname:
            template_fname = utils.get_resource_path('CSDGM_Template.xml')
            shutil.copyfile(template_fname, save_as_fname)
            self.load_file(save_as_fname)
            self.set_current_file(save_as_fname)
            self.update_recent_file_actions()

            today = fgdc_utils.format_date(datetime.datetime.now())
            self.metadata_root.metainfo.metd.set_date(today)

    def set_template(self):
        fname = self.get_xml_fname()

        if fname:
            settings = QSettings('USGS', 'pymdwizard')
            settings.setValue('template_fname', fname)
            just_fname = os.path.split(fname)[-1]
            self.ui.actionCurrentTemplate.setText('Current: ' + just_fname)

    def restore_template(self):
        settings = QSettings('USGS', 'pymdwizard')
        fname = utils.get_resource_path('CSDGM_Template.xml')
        settings.setValue('template_fname', None)
        self.ui.actionCurrentTemplate.setText('Current: Built-in')

    def load_default(self):
        settings = QSettings('USGS', 'pymdwizard')
        template_fname = settings.value('template_fname')

        if template_fname is None:
            template_fname = utils.get_resource_path('CSDGM_Template.xml')
        elif not os.path.exists(template_fname):
            msg = "The previous template file specified, {}, could not be " \
                  "found.".format(template_fname)
            msg += "\nCheck that the file has not beed deleted, renamed " \
                   "or moved."
            msg += "Defaulting to the built in template.".format(template_fname)
            QMessageBox.warning(self, "Template file missing", msg)
            template_fname = utils.get_resource_path('CSDGM_Template.xml')

        self.load_file_content(template_fname)
        self.cur_fname = ''

        today = fgdc_utils.format_date(datetime.datetime.now())
        self.metadata_root.metainfo.metd.set_date(today)

    def set_current_file(self, fname):
        """
        The procedure for storing and displaying a new current file
        The following get done:
        1 - Display the file name without path in the apps title bar
        2 - Insert the file name into the top slot of the recent files
        3 - Save this list out to the setting variable

        Parameters
        ----------
        fname : str
            The file name and path that will be used

        Returns
        -------
        None
        """
        self.cur_fname = fname
        if fname:
            stripped_name = QFileInfo(fname).fileName()
            title = "Metadata Wizard - {}".format(stripped_name)
            self.setWindowTitle(title)

            settings = QSettings('USGS', 'pymdwizard')
            files = settings.value('recentFileList', [])

            try:
                files.remove(fname)
            except ValueError:
                pass

            files.insert(0, fname)
            del files[PyMdWizardMainForm.max_recent_files:]

            settings.setValue('recentFileList', files)

            for widget in QApplication.topLevelWidgets():
                if isinstance(widget, PyMdWizardMainForm):
                    widget.update_recent_file_actions()
        else:
            self.setWindowTitle("Metadata Wizard")

    def update_recent_file_actions(self):
        """
        Update the actions (menu items) in the recent files list to
        reflect the recent file paths stored in the 'recentFileList' setting

        Returns
        -------
        None
        """
        settings = QSettings('USGS', 'pymdwizard')
        files = settings.value('recentFileList', [])

        num_recent_files = min(len(files), PyMdWizardMainForm.max_recent_files)

        for i in range(num_recent_files):
            stripped_name = QFileInfo(files[i]).fileName()
            text = "&%d %s" % (i + 1, stripped_name)
            self.recent_file_actions[i].setText(text)
            self.recent_file_actions[i].setData(files[i])
            self.recent_file_actions[i].setVisible(True)

        for j in range(num_recent_files, PyMdWizardMainForm.max_recent_files):
            self.recent_file_actions[j].setVisible(False)

    def check_for_changes(self):
        try:
            if self.cur_fname and os.path.exists(self.cur_fname):
                cur_xml = xml_utils.node_to_string(self.metadata_root.to_xml())
                disk_xml = xml_utils.node_to_string(xml_utils.fname_to_node(self.cur_fname))

            if cur_xml != disk_xml:
                msg = "Do you want to save your changes?"
                alert = QDialog()
                self.last_updated = time.time()
                confirm = QMessageBox.question(self, "Save Changes", msg, QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel)
                if confirm == QMessageBox.Yes:
                    xml_utils.save_to_file(self.metadata_root.to_xml(), self.cur_fname)
                elif confirm == QMessageBox.Cancel:
                    return 'Cancel'
                self.cur_fname = ''
        except:
            pass
        return None

    def exit(self):
        """
        Before exiting check if the current contents match what is on the
        file system.  If the do not match ask user if they would like to
        save or cancel the exit

        Returns
        -------
        str :
        'Close' or 'Cancel' depending on user choice.
        """
        changed = self.check_for_changes()
        if changed == 'Cancel':
            return changed
        else:
            self.close()
            return 'Close'

    def closeEvent(self, event):
        """ Intercept the builtin closeEvent so that we can check for
        changes and ask if we should change.

        Parameters
        ----------
        event

        Returns
        -------

        """
        if self.exit() == 'Close':
            event.accept()
        else:
            event.ignore()

    def use_dataqual(self, sender=None):
        self.metadata_root.use_section('dataqual', sender)

    def use_spatial(self, sender=None):
        self.metadata_root.use_section('spatial', sender)

    def use_eainfo(self, sender=None):
        self.metadata_root.use_section('eainfo', sender)

    def use_distinfo(self, sender=None):
        self.metadata_root.use_section('distinfo', sender)

    def clear_validation(self):
        """
        Remove the error highlighting from all the error widgets

        Returns
        -------
        None
        """
        annotation_lookup = fgdc_utils.get_fgdc_lookup()

        for widget in self.error_widgets:
            if not sip.isdeleted(widget) and \
                    widget.objectName() not in ['metadata_root', 'fgdc_metadata']:
                widget.setStyleSheet("""""")
                print(widget.objectName())
                shortname = widget.objectName().replace('fgdc_', '')
                if shortname[-1].isdigit():
                    shortname = shortname[:-1]
                try:
                    widget.setToolTip(annotation_lookup[shortname]['annotation'])
                except KeyError:
                    widget.setToolTip('')

        self.error_widgets = []
        self.error_list.clear_errors()
        self.error_list_dialog.hide()

    def validate(self):
        """
        Check the current record against the schema and highlight any
        error widgets

        Returns
        -------
        None
        """

        self.error_list_dialog.show()
        if self.metadata_root.schema == 'bdp':
            xsl_fname = utils.get_resource_path('FGDC/BDPfgdc-std-001-1998-annotated.xsd')
        else:
            xsl_fname = utils.get_resource_path('FGDC/fgdc-std-001-1998-annotated.xsd')
        from pymdwizard.core import fgdc_utils
        errors = fgdc_utils.validate_xml(self.metadata_root.to_xml(), xsl_fname)

        self.clear_validation()

        marked_errors = []

        # We need to expand / populate all attributes that have an error
        for error in errors:
            try:
                xpath, error_msg, line_num = error
                if 'attr' in xpath:
                    try:
                        detailed_index = xpath.split('/detailed[')[1].split('/')[0][:-1]
                        detailed_index = int(detailed_index)-1
                    except IndexError:
                        detailed_index = 0

                    try:
                        attr_index = xpath.split('/attr[')[1].split('/')[0][:-1]
                        attr_index = int(attr_index)-1
                    except IndexError:
                        attr_index = 0

                    self.metadata_root.eainfo.detaileds[detailed_index].attributes.attrs[attr_index].regular_me()
                    self.metadata_root.eainfo.detaileds[detailed_index].attributes.attrs[attr_index].supersize_me()
            except:
                pass

        widget_lookup = self.metadata_root.make_tree(widget=self.metadata_root)
        self.metadata_root.add_children(self.metadata_root.spatial_tab, widget_lookup.metadata.idinfo)
        self.metadata_root.add_children(self.metadata_root.dataqual.sourceinput, widget_lookup.metadata.dataqual.lineage)
        error_count = 0
        for error in errors:

            try:
                xpath, error_msg, line_num = error
                if xpath not in marked_errors:
                    self.error_list.add_error(error_msg, xpath)
                    marked_errors.append(xpath)

                    # widget = self.metadata_root.get_widget(xpath)
                    widgets = widget_lookup.xpath_march(xpath, as_list=True)
                    for widget in widgets:
                        if isinstance(widget, list):
                            for w in widget:
                                print('problem highlighting error', xpath, widget)
                        else:
                            self.highlight_error(widget.widget, error_msg)
                            self.error_widgets.append(widget.widget)
                            error_count += 1
            except BaseException as e:
                import traceback
                msg = "Error encountered highlighting error:"
                msg += "\t" + xpath
                msg += "\n\n" + traceback.format_exc()
                QMessageBox.warning(self, "Bug encountered", msg)
        widget_lookup = self.metadata_root.make_tree(widget=self.metadata_root)
        if errors:
            msg = "There are {} errors in this record".format(error_count)
            self.statusBar().showMessage(msg, 20000)
            msg += "\n\n These errors are highlighted in red in the form below."
            msg += "\n\n These errors are also listed in the Validation Errors Form that just popped up."
            msg += "\n Clicking each error will take you to the section it is contained in."
            msg += "\n Note that some highlighed errors can be in collapsed items, scrolled out of view, or in non-selected tabs"
            QMessageBox.warning(self, "Validation", msg)
            self.error_list_dialog.show()
        else:
            msg = "Congratulations there were No FGDC Errors!"
            self.statusBar().showMessage(msg, 20000)
            QMessageBox.information(self, "Validation", msg)

    def goto_error(self, sender):
        """
        super highlight the selected error and switch the tab to the section
        that contains this error.

        Parameters
        ----------
        sender : QWidget

        Returns
        -------
        None
        """

        xpath = sender.data(1)
        section = xpath.split('/')[1]

        if section == 'idinfo':
            subsection = xpath.split('/')[2]
            if subsection == 'spdom':
                parent_section = self.metadata_root.switch_section(2)
            else:
                parent_section = self.metadata_root.switch_section(0)
        elif section == 'dataqual':
            parent_section = self.metadata_root.switch_section(1)
        elif section == 'spdoinfo' or section == 'spref':
            parent_section = self.metadata_root.switch_section(2)
        elif section == 'eainfo':
            parent_section = self.metadata_root.switch_section(3)
        elif section == 'eainfo':
            parent_section = self.metadata_root.switch_section(3)
        elif section == 'distinfo':
            parent_section = self.metadata_root.switch_section(4)
        elif section == 'metainfo':
            parent_section = self.metadata_root.switch_section(5)

        if self.last_highlight is not None and \
                not sip.isdeleted(self.last_highlight):
            self.highlight_error(self.last_highlight,
                                          self.last_highlight.toolTip())

        widget_lookup = self.metadata_root.make_tree(widget=self.metadata_root)
        bad_widget = widget_lookup.xpath_march(xpath, as_list=True)

        try:
            parent_wizwidget = [thing for thing in parent_section.children()
                                if isinstance(thing, WizardWidget)][0]
            parent_wizwidget.scroll_area.ensureWidgetVisible(bad_widget[0].widget)
        except:
            pass


        self.last_highlight = bad_widget[0].widget
        self.highlight_error(bad_widget[0].widget, sender.text(), superhot=True)

    def highlight_error(self, widget, error_msg, superhot=False):
        """
        Highlight the given widget and set it's tooltip msg to error_msg

        Parameters
        ----------
        widget : QWidget
        error_msg : str
                    the message that will appear in the tooltip
        superhot : bool
            whether to use the regular highlight
            or also include a black thick outline

        Returns
        -------
            None
        """

        if widget.objectName() in ['fgdc_attr', 'fgdc_edomv', 'fgdc_edomvd',
                                   'fgdc_edomvds', 'fgdc_attrlabl',
                                   'fgdc_attrdef', 'fgdc_attrdefs',
                                   'fgdc_codesetd', 'fgdc_edom', 'fgdc_rdom',
                                   'fgdc_udom', 'fgdc_rdommin', 'fgdc_rdommax',
                                   'fgdc_codesetn', 'fgdc_codesets',
                                   'fgdc_attrdomv',]:
            self.highlight_attr(widget)

        if widget.objectName() in ['fgdc_themekey', 'fgdc_themekt',
                                   'fgdc_placekey', 'fgdc_placekt',
                                   'fgdc_procdesc', 'fgdc_srcused',
                                   'fgdc_srcprod']:
            self.highlight_tab(widget)

        if superhot:
            color = "rgb(223,1,74)"
            lw = "border: 3px solid black;"
        else:
            color = 'rgb(223,1,74)'
            lw = ''

        color = "rgb(225,67,94)"

        if widget.objectName() not in ['metadata_root', 'fgdc_metadata']:
            try:
                widget.setToolTip(error_msg)
                widget.setStyleSheet(
                            """
                QGroupBox#{widgetname}{{
                background-color: {color};
                border: 2px solid red;
                subcontrol-position: top left; /* position at the top left*/
                padding-top: 20px;
                font: bold 14px;
                color: rgb(90, 90, 90);
                }}
                QGroupBox#{widgetname}::title {{
                text-align: left;
                subcontrol-origin: padding;
                subcontrol-position: top left; /* position at the top center */padding: 3 3px;
                }}
                QLabel{{
                font: 9pt "Arial";
                color: rgb(90, 90, 90);
                }}
                QLineEdit#{widgetname}, QPlainTextEdit#{widgetname}, QComboBox#{widgetname} {{
                font: 9pt "Arial";
                color: rgb(50, 50, 50);
                background-color: {color};
                opacity: 25;
                {lw}
                }}
                QToolTip {{
                background-color: rgb(255,76,77);
                border-color: red;
                opacity: 255;
                }}
                """.format(widgetname=widget.objectName(), color=color, lw=lw))
            except:
                pass



    def highlight_attr(self, widget):
        widget_parent = widget
        attr_frame = widget

        while not widget_parent.objectName() == 'fgdc_attr':
            widget_parent = widget_parent.parent()
            attr_frame = widget_parent
        self.error_widgets.append(attr_frame)
        widget_parent = widget_parent.parent()

        widget_parent.supersize_me()
        error_msg = "'Validation error in hidden contents, click to show'"
        widget_parent.setToolTip(error_msg)
        widget_parent.setStyleSheet(
            """
    QFrame#{widgetname}{{
    border: 2px solid red;
    }}
        """.format(widgetname=attr_frame.objectName()))

        self.error_widgets.append(widget_parent)

    def highlight_tab(self, widget):

        widget_parent = widget.parent()
        while not type(widget_parent) == QTabWidget:
            widget_parent = widget_parent.parent()

        error_msg = "'Validation error in hidden contents, click to show'"
        widget_parent.setToolTip(error_msg)
        widget_parent.setStyleSheet(
            """
    QTabBar {{
    background-color: rgb(225,67,94);
    qproperty-drawBase:0;

}}
        """)

        self.error_widgets.append(widget_parent)

    def dragEnterEvent(self, e):
        if e.mimeData().hasUrls:
            e.accept()
        else:
            e.ignore()

    def dragMoveEvent(self, e):
        if e.mimeData().hasUrls() and e.mimeData().urls()[0].isLocalFile():
            e.accept()
        else:
            e.ignore()

    def dropEvent(self, e):
        """
        Drop files directly onto the widget
        File locations are stored in fname
        :param e:
        :return:
        """
        if e.mimeData().hasUrls:
            e.setDropAction(Qt.CopyAction)

            url = e.mimeData().urls()[0]
            fname = url.toLocalFile()
            if os.path.isfile(fname):
                self.open_file(fname)
            e.accept()
        else:
            e.ignore()

    def preview(self):
        """
        Shows a preview window with the xml content rendered using stylesheet

        Returns
        -------
        None
        """

        xsl_fname = utils.get_resource_path("FGDC/FGDC_Stylesheet.xsl")
        transform = xml_utils.load_xslt(xsl_fname)
        result = transform(self.metadata_root.to_xml())

        tmp = tempfile.NamedTemporaryFile(suffix='.html')
        tmp.close()
        result.write(tmp.name)

        self.preview = Preview(url=tmp.name)

        self.preview_dialog = QDialog(self)
        self.preview_dialog.setWindowTitle('Metadata Preview')
        self.preview_dialog.setLayout(self.preview.layout())

        self.preview_dialog.exec_()

    def launch_help(self):
        this_fname = os.path.realpath(__file__)
        gui_dname = os.path.dirname(this_fname)
        pmdwiz_dname = os.path.dirname(gui_dname)
        root_dname = os.path.dirname(pmdwiz_dname)
        help_html = os.path.join(root_dname, 'docs', 'html_output', 'index.html')

        self.preview = Preview(url=help_html)
        os.path.dirname(this_fname)

        self.preview_dialog = QDialog(self)
        self.preview_dialog.setWindowTitle('MetadataWizard Help')
        self.preview_dialog.setLayout(self.preview.layout())

        self.preview_dialog.exec_()

    def generate_review_doc(self):
        if self.cur_fname:
            out_fname = self.cur_fname[:-4] + '_REVIEW.docx'

            if self.metadata_root.schema == 'bdp':
                which = 'bdp'
            else:
                which = 'fgdc'

            if time.time() - self.last_updated > 4:
                msg = "Would you like to save the current file before continuing?"
                alert = QDialog()
                self.last_updated = time.time()
                confirm = QMessageBox.question(self, "File save", msg,
                                               QMessageBox.Yes | QMessageBox.No
                                               | QMessageBox.Cancel)
                if confirm == QMessageBox.Yes:
                    self.save_file()
                elif confirm == QMessageBox.Cancel:
                    return
            try:
                cur_content = xml_utils.XMLRecord(self.cur_fname)
                review_utils.generate_review_report(cur_content, out_fname,
                                                    which=which)

                import subprocess
                os.startfile('"{}"'.format(out_fname))
                msg = 'Review document available at: {}'.format(out_fname)
                msg += '\n\nReview document now opening in default application...'
                QMessageBox.information(self, "Review finished", msg)
            except BaseException:
                import traceback
                msg = "Problem encountered generating review document:\n{}".format(traceback.format_exc())
                QMessageBox.warning(self, "Problem encountered", msg)

    def launch_jupyter(self):
        """
        Launches a jupyter notebook server in our examples directory

        Returns
        -------
        None
        """


        settings = QSettings('USGS', 'pymdwizard')
        last_kernel = settings.value('last_kernel', '')
        jupyter_dnames = settings.value('jupyter_dnames', [])
        if not jupyter_dnames:
            install_dir = utils.get_install_dname()
            jupyter_dnames = [os.path.join(install_dir, 'examples')]
            settings.setValue('jupyter_dnames', jupyter_dnames)

        self.jupyter_dialog = JupyterStarter(last_kernel=last_kernel,
                                             previous_dnames=jupyter_dnames,
                                    update_function=self.update_jupyter_dnames)
        utils.set_window_icon(self.jupyter_dialog)
        self.jupyter_dialog.show()

    def update_jupyter_dnames(self, kernel, dname):
        settings = QSettings('USGS', 'pymdwizard')
        jupyter_dnames = settings.value('jupyter_dnames', [])

        try:
            jupyter_dnames.remove(dname)
        except ValueError:
            pass

        jupyter_dnames.insert(0, dname)
        del jupyter_dnames[PyMdWizardMainForm.max_recent_files:]
        settings.setValue('jupyter_dnames', jupyter_dnames)

        settings.setValue('last_kernel', kernel)

    def about(self):

        msgbox = QMessageBox(self)
        msgbox.setWindowTitle("About")
        msgbox.setTextFormat(Qt.RichText)

        msg = 'The MetadataWizard was developed by the USGS Fort Collins Science Center<br>'
        msg += 'With help from the USGS Council for Data integration (CDI) and<br>'
        msg += 'and the USGS Core Science Analytics, Synthesis, and Libraries (CSAS&L)<br>'
        msg += "<br> Project page: <a href='https://github.com/usgs/fort-pymdwizard'>https://github.com/usgs/fort-pymdwizard</a>"
        msg += '<br><br>Contact: Colin Talbert at [email protected]'
        msgbox.setText(msg)
        msgbox.exec_()

    def check_for_updates(self):
        from subprocess import check_output

        install_dir = utils.get_install_dname()
        root_dir = os.path.dirname(install_dir)
        git_exe = os.path.join(root_dir, 'Python36_64', 'Library', 'bin', 'git.exe')

        try:
            fetch = check_output([git_exe, 'fetch', 'usgs_root'], cwd=install_dir, shell=True)
            updates = check_output([git_exe, 'log', 'HEAD..usgs_root/master'], cwd=install_dir, shell=True)
            if updates:
                msg = "An update(s) are available for the Metadata Wizard.\n"
                msg += "Would you like to install these now?"

                confirm = QMessageBox.question(self, "Updates Available", msg,
                                               QMessageBox.Yes|QMessageBox.No)
                if confirm == QMessageBox.Yes:
                    self.update_from_github()
        except BaseException as e:
            pass

    def update_from_github(self):
        from subprocess import check_output

        install_dir = utils.get_install_dname()
        root_dir = os.path.dirname(install_dir)
        update_bat = os.path.join(root_dir, 'update_wizard.bat')
        if os.path.exists(update_bat) and os.path.exists(root_dir):
            try:
                QApplication.setOverrideCursor(Qt.WaitCursor)
                p = check_output([update_bat], cwd=root_dir, shell=False)
                if p.splitlines()[-1] == b'Already up-to-date.':
                    msg = 'Application already up to date.'
                else:
                    msg = 'Application updated.\n\n'
                    msg += 'Please close and restart the Wizard for these updates to take effect'
                QApplication.restoreOverrideCursor()
            except BaseException as e:
                import traceback
                msg = "Could not update application:\n{}".format(traceback.format_exc())
                QApplication.restoreOverrideCursor()
                QMessageBox.warning(self, "Recent Files", msg)

        else:
            msg = 'Could not find the batch file to update the application'
        QApplication.restoreOverrideCursor()
        QMessageBox.information(self, "Update results", msg)
Пример #3
0
class PyMdWizardMainForm(QMainWindow):

    max_recent_files = 10

    def __init__(self, parent=None):
        super(self.__class__, self).__init__()

        self.cur_fname = ''
        self.file_watcher = None

        # list of buttons for opening recently accessed files
        self.recent_file_actions = []
        # list of widgets that are currently styled as errors
        self.error_widgets = []
        # the last error widget that was highlighted
        self.last_highlight = None

        self.build_ui()
        self.connect_events()

        self.load_default()

    def build_ui(self):
        """
        Build and modify this widget's GUI

        Returns
        -------
        None
        """
        self.ui = UI_MainWindow.Ui_MainWindow()
        self.ui.setupUi(self)

        utils.set_window_icon(self)

        self.metadata_root = MetadataRoot()
        self.ui.centralwidget.layout().addWidget(self.metadata_root)

        for i in range(PyMdWizardMainForm.max_recent_files):
            self.recent_file_actions.append(
                QAction(self, visible=False,
                        triggered=self.open_recent_file))
            self.ui.menuRecent_Files.addAction(self.recent_file_actions[i])
        self.update_recent_file_actions()

        self.ui.menuErrors.clear()

        settings = QSettings('USGS', 'pymdwizard')
        template_fname = settings.value('template_fname')

        if template_fname is not None:
            just_fname = os.path.split(template_fname)[-1]
            self.ui.actionCurrentTemplate.setText('Current: ' + just_fname)

    def connect_events(self):
        """
        Connect the appropriate GUI components with the corresponding functions

        Returns
        -------
        None
        """
        self.ui.actionOpen.triggered.connect(self.open_file)
        self.ui.actionSave.triggered.connect(self.save_file)
        self.ui.actionSave_as.triggered.connect(self.save_as)
        self.ui.actionExit.triggered.connect(self.exit)
        self.ui.actionRun_Validation.triggered.connect(self.validate)
        self.ui.actionClear_validation.triggered.connect(self.clear_validation)
        self.ui.actionPreview.triggered.connect(self.preview)
        self.ui.actionNew.triggered.connect(self.new_record)
        self.ui.actionBrowseTemplate.triggered.connect(self.set_template)
        self.ui.actionRestoreBuiltIn.triggered.connect(self.restore_template)
        self.ui.actionLaunch_Jupyter.triggered.connect(self.launch_jupyter)
        self.ui.actionUpdate.triggered.connect(self.update_from_github)

    def open_recent_file(self):
        """
        handles the opening of a recent file selection
        Returns
        -------
        None
        """
        action = self.sender()
        if action:
            self.load_file(action.data())
            self.set_current_file(action.data())

    def get_xml_fname(self):
        """

        Returns
        -------
        str: path and filename of the selected file or
        empty string if none was selected
        """
        settings = QSettings('USGS', 'pymdwizard')
        recent_files = settings.value('recentFileList', [])
        if recent_files:
            dname, fname = os.path.split(recent_files[0])
        else:
            fname, dname = "", ""

        fname = QFileDialog.getOpenFileName(self, fname, dname, \
                                            filter="XML Files (*.xml)")

        if fname[0]:
            return fname[0]
        else:
            return ''

    def open_file(self, fname=None):
        """
        Browse to a file and load it if it is acceptable

        Returns
        -------
        None
        """
        if fname is None or not fname:
            fname = self.get_xml_fname()

        if fname:
            self.load_file(fname)
            self.set_current_file(fname)
            self.update_recent_file_actions()

    def load_file(self, fname):
        """
        load a file's content into the application.

        Parameters
        ----------
        fname : str
                full file path and name of the file to load
        Returns
        -------
        None
        """
        self.file_watcher = QFileSystemWatcher([fname])
        self.file_watcher.fileChanged.connect(self.file_updated)
        self.last_updated = time.time()

        self.clear_validation()

        #check that we have read write access to the file
        file = QFile(fname)
        if not file.open(QFile.ReadOnly | QFile.Text):
            msg = "Cannot read file %s:\n%s." % (fname, file.errorString())
            QMessageBox.warning(self, "Recent Files", msg)
            return
        file.close()

        self.load_file_content(fname)

    def load_file_content(self, fname):
        QApplication.setOverrideCursor(Qt.WaitCursor)
        QApplication.processEvents()
        exc_info = sys.exc_info()
        try:
            new_record = etree.parse(fname)
            self.metadata_root._from_xml(new_record)
            self.statusBar().showMessage("File loaded", 10000)
        except BaseException as e:
            import traceback
            msg = "Cannot open file %s:\n%s." % (fname, traceback.format_exc())
            QMessageBox.warning(self, "Recent Files", msg)
        QApplication.restoreOverrideCursor()


    def file_updated(self):
        """
        The function that fires when the file watcher detects that the
        current file has changed on the file system.  Prompts the user and
        loads the new file if they choose to.

        Returns
        -------
        None
        """
        if time.time() - self.last_updated > 4:
            msg = "The file you are editing has been changed on disk.  Would you like to reload this File?"
            alert = QDialog()
            self.last_updated = time.time()
            confirm = QMessageBox.question(self, "File Changed", msg, QMessageBox.Yes | QMessageBox.No)
            if confirm == QMessageBox.Yes:
                self.load_file(self.cur_fname)

    def save_as(self):
        """
        Navigate to a new or existing file and save the current document
        into this file.

        Returns
        -------
        None
        """
        fname = self.get_save_name()
        if fname:
            self.set_current_file(fname)
            self.update_recent_file_actions()
            self.save_file()

    def get_save_name(self):
        """
        launches a saveas dialog to browse to a file to save to.
        starts in the directory of the most recently sopened file

        Returns
        -------
        str: file name and path
        """
        settings = QSettings('USGS', 'pymdwizard')
        recent_files = settings.value('recentFileList', [])
        if recent_files:
            dname, fname = os.path.split(recent_files[0])
        else:
            fname, dname = "", ""

        fname = QFileDialog.getSaveFileName(self, "Save As", dname, \
                                            filter="XML Files (*.xml)")

        return fname[0]

    def save_file(self):
        """
        Save the current xml document.  Prompts for a filename if one
        has not been set yet.

        Returns
        -------
        None
        """
        if not self.cur_fname:
            fname = self.get_save_name()
            if not fname:
                return
        else:
            fname = self.cur_fname

        fname_msg = utils.check_fname(fname)
        if not fname_msg == 'good':
            msg = "Cannot write to :\n  {}.".format(fname)
            QMessageBox.warning(self, "Metadata Wizard", msg)
            return

        xml_utils.save_to_file(self.metadata_root._to_xml(), fname)
        self.last_updated = time.time()

        self.set_current_file(fname)
        self.statusBar().showMessage("File saved", 2000)

    def new_record(self):
        """
        Create a new record.
        Starts by making a copy of the template file 'CSDGM_Template.xml'.
            in the resources folder to a name selected in a save as dialog.
        Then updates the MD date to today.
        Returns
        -------
        None
        """
        self.load_default()
        save_as_fname = self.get_save_name()
        if save_as_fname:
            template_fname = utils.get_resource_path('CSDGM_Template.xml')
            shutil.copyfile(template_fname, save_as_fname)
            self.load_file(save_as_fname)
            self.set_current_file(save_as_fname)
            self.update_recent_file_actions()

            today = fgdc_utils.format_date(datetime.datetime.now())
            self.metadata_root.metainfo.metd.set_date(today)

    def set_template(self):
        fname = self.get_xml_fname()

        if fname:
            settings = QSettings('USGS', 'pymdwizard')
            settings.setValue('template_fname', fname)
            just_fname = os.path.split(fname)[-1]
            self.ui.actionCurrentTemplate.setText('Current: ' + just_fname)

    def restore_template(self):
        settings = QSettings('USGS', 'pymdwizard')
        fname = utils.get_resource_path('CSDGM_Template.xml')
        settings.setValue('template_fname', None)
        self.ui.actionCurrentTemplate.setText('Current: Built-in')

    def load_default(self):
        settings = QSettings('USGS', 'pymdwizard')
        template_fname = settings.value('template_fname')

        if template_fname is None:
            template_fname = utils.get_resource_path('CSDGM_Template.xml')

        self.load_file_content(template_fname)
        self.cur_fname = ''

        today = fgdc_utils.format_date(datetime.datetime.now())
        self.metadata_root.metainfo.metd.set_date(today)

    def set_current_file(self, fname):
        """
        The procedure for storing and displaying a new current file
        The following get done:
        1 - Display the file name without path in the apps title bar
        2 - Insert the file name into the top slot of the recent files
        3 - Save this list out to the setting variable

        Parameters
        ----------
        fname : str
            The file name and path that will be used

        Returns
        -------
        None
        """
        self.cur_fname = fname
        if fname:
            stripped_name = QFileInfo(fname).fileName()
            title = "Metadata Wizard - {}".format(stripped_name)
            self.setWindowTitle(title)

            settings = QSettings('USGS', 'pymdwizard')
            files = settings.value('recentFileList', [])

            try:
                files.remove(fname)
            except ValueError:
                pass

            files.insert(0, fname)
            del files[PyMdWizardMainForm.max_recent_files:]

            settings.setValue('recentFileList', files)

            for widget in QApplication.topLevelWidgets():
                if isinstance(widget, PyMdWizardMainForm):
                    widget.update_recent_file_actions()
        else:
            self.setWindowTitle("Metadata Wizard")

    def update_recent_file_actions(self):
        """
        Update the actions (menu items) in the recent files list to
        reflect the recent file paths stored in the 'recentFileList' setting

        Returns
        -------
        None
        """
        settings = QSettings('USGS', 'pymdwizard')
        files = settings.value('recentFileList', [])

        num_recent_files = min(len(files), PyMdWizardMainForm.max_recent_files)

        for i in range(num_recent_files):
            stripped_name = QFileInfo(files[i]).fileName()
            text = "&%d %s" % (i + 1, stripped_name)
            self.recent_file_actions[i].setText(text)
            self.recent_file_actions[i].setData(files[i])
            self.recent_file_actions[i].setVisible(True)

        for j in range(num_recent_files, PyMdWizardMainForm.max_recent_files):
            self.recent_file_actions[j].setVisible(False)

    def exit(self):
        """
        Before exiting check if the current contents match what is on the
        file system.  If the do not match ask user if they would like to
        save or cancel the exit

        Returns
        -------
        str :
        'Close' or 'Cancel' depending on user choice.
        """
        if self.cur_fname:
            cur_xml = xml_utils.node_to_string(self.metadata_root._to_xml())
            disk_xml = xml_utils.node_to_string(xml_utils.fname_to_node(self.cur_fname))

            if cur_xml != disk_xml:
                msg = "Would you like to save before exiting?"
                alert = QDialog()
                self.last_updated = time.time()
                confirm = QMessageBox.question(self, "File Changed", msg, QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel)
                if confirm == QMessageBox.Yes:
                    self.save_file()
                elif confirm == QMessageBox.Cancel:
                    return 'Cancel'
                self.cur_fname = ''

        self.close()
        return 'Close'

    def closeEvent(self, event):
        """ Intercept the builtin closeEvent so that we can check for
        changes and ask if we should change.

        Parameters
        ----------
        event

        Returns
        -------

        """
        if self.exit() == 'Close':
            event.accept()
        else:
            event.ignore()

    def clear_validation(self):
        """
        Remove the error highlighting from all the error widgets

        Returns
        -------
        None
        """
        self.ui.menuErrors.clear()

        annotation_lookup_fname = utils.get_resource_path("FGDC/bdp_lookup")
        with open(annotation_lookup_fname, encoding='utf-8') as data_file:
            annotation_lookup = json.loads(data_file.read())

        for widget in self.error_widgets:

            if not sip.isdeleted(widget) and \
                    widget.objectName() not in ['metadata_root', 'fgdc_metadata']:
                widget.setStyleSheet("""""")
                shortname = widget.objectName().replace('fgdc_', '')
                if shortname[-1].isdigit():
                    shortname = shortname[:-1]
                widget.setToolTip(annotation_lookup[shortname]['annotation'])

        self.error_widgets = []

    def validate(self):
        """
        Check the current record against the schema and highlight any
        error widgets

        Returns
        -------
        None
        """

        if self.metadata_root.schema == 'bdp':
            xsl_fname = utils.get_resource_path('FGDC/BDPfgdc-std-001-1998-annotated.xsd')
        else:
            xsl_fname = utils.get_resource_path('FGDC/fgdc-std-001-1998-annotated.xsd')
        from pymdwizard.core import fgdc_utils
        errors = fgdc_utils.validate_xml(self.metadata_root._to_xml(), xsl_fname)

        self.clear_validation()

        marked_errors = []

        self.widget_lookup = self.metadata_root.make_tree(widget=self.metadata_root)

        error_count = 0
        for error in errors:
            xpath, error_msg, line_num = error
            if xpath not in marked_errors:

                action = QAction(self, visible=True)
                action.setText(error_msg)
                action.setData(xpath)
                action.triggered.connect(self.goto_error)
                self.ui.menuErrors.addAction(action)
                marked_errors.append(xpath)

                # widget = self.metadata_root.get_widget(xpath)
                widgets = self.widget_lookup.xpath_march(xpath, as_list=True)
                for widget in widgets:
                    if isinstance(widget, list):
                        for w in widget:
                            print('problem highlighting error', xpath, widget)
                    else:
                        self.highlight_error(widget.widget, error_msg)
                        self.error_widgets.append(widget.widget)
                        error_count += 1

        if errors:
            msg = "There are {} errors in this record".format(error_count)
            self.statusBar().showMessage(msg, 20000)
            msg += "\n\n These errors are highlighted in red in the form below."
            msg += "\n\n These errors are also listed in the Validation Menu's Errors submenu item above."
            msg += "\n Clicking each error will take you to the section it is contained in."
            msg += "\n Note that some highlighed errors can be in collapsed items, scrolled out of view, or in non-selected tabs"
            QMessageBox.warning(self, "Validation", msg)
        else:
            msg = "Congratulations there were No FGDC Errors!"
            self.statusBar().showMessage(msg, 20000)
            QMessageBox.information(self, "Validation", msg)

    def goto_error(self, sender):
        """
        super highlight the selected error and switch the tab to the section
        that contains this error.

        Parameters
        ----------
        sender : QWidget

        Returns
        -------
        None
        """

        xpath = self.sender().data()
        section = xpath.split('/')[1]

        if section == 'idinfo':
            subsection = xpath.split('/')[2]
            if subsection == 'spdom':
                self.metadata_root.switch_section(2)
            else:
                self.metadata_root.switch_section(0)
        elif section == 'dataqual':
            self.metadata_root.switch_section(1)
        elif section == 'spdoinfo' or section == 'spref':
            self.metadata_root.switch_section(2)
        elif section == 'eainfo':
            self.metadata_root.switch_section(3)
        elif section == 'eainfo':
            self.metadata_root.switch_section(3)
        elif section == 'distinfo':
            self.metadata_root.switch_section(4)
        elif section == 'metainfo':
            self.metadata_root.switch_section(5)

        if self.last_highlight is not None and \
                not sip.isdeleted(self.last_highlight):
            self.highlight_error(self.last_highlight, self.last_highlight.toolTip())

        bad_widget = self.widget_lookup.xpath_march(xpath, as_list=True)
        self.last_highlight = bad_widget[0].widget
        self.highlight_error(bad_widget[0].widget, self.sender().text(), superhot=True)

    def highlight_error(self, widget, error_msg, superhot=False):
        """
        Highlight the given widget and set it's tooltip msg to error_msg

        Parameters
        ----------
        widget : QWidget
        error_msg : str
                    the message that will appear in the tooltip
        superhot : bool
            whether to use the regular highlight
            or also include a black thick outline

        Returns
        -------
            None
        """

        if widget.objectName() in ['fgdc_edomv', 'fgdc_edomvd', 'fgdc_edomvds',
                                   'fgdc_attrlabl', 'fgdc_attrdef',
                                   'fgdc_attrdefs', 'fgdc_attrdomv',
                                   'fgdc_codesetd', 'fgdc_edom', 'fgdc_rdom',
                                   'fgdc_udom', 'fgdc_rdommin', 'fgdc_rdommax',
                                   'fgdc_codesetn', 'fgdc_codesets']:
            self.highlight_attr(widget)

        if widget.objectName() in ['fgdc_themekey', 'fgdc_themekt',
                                   'fgdc_placekey', 'fgdc_placekt',
                                   'fgdc_procdesc', 'fgdc_srcused',
                                   'fgdc_srcprod']:
            self.highlight_tab(widget)


        if superhot:
            color = "rgb(223,1,74)"
            lw = "border: 3px solid black;"
        else:
            color = 'rgb(223,1,74)'
            lw = ''

        color = "rgb(225,67,94)"

        if widget.objectName() not in ['metadata_root', 'fgdc_metadata']:
            widget.setToolTip(error_msg)
            widget.setStyleSheet(
                """
        QGroupBox#{widgetname}{{
        background-color: {color};
        border: 2px solid red;
        subcontrol-position: top left; /* position at the top left*/
        padding-top: 20px;
        font: bold 14px;
        color: rgb(90, 90, 90);
        }}
        QGroupBox#{widgetname}::title {{
        text-align: left;
        subcontrol-origin: padding;
        subcontrol-position: top left; /* position at the top center */padding: 3 3px;
        }}
        QLabel{{
        font: 9pt "Arial";
        color: rgb(90, 90, 90);
        }}
        QLineEdit#{widgetname}, QPlainTextEdit#{widgetname}, QComboBox#{widgetname} {{
        font: 9pt "Arial";
        color: rgb(50, 50, 50);
        background-color: {color};
        opacity: 25;
        {lw}
        }}
        QToolTip {{
        background-color: rgb(255,76,77);
        border-color: red;
        opacity: 255;
        }}
        """.format(widgetname=widget.objectName(), color=color, lw=lw))



    def highlight_attr(self, widget):
        widget_parent = widget.parent()

        while not widget_parent.objectName() == 'fgdc_attr':
            widget_parent = widget_parent.parent()

        error_msg = "'Validation error in hidden contents, click to show'"
        widget_parent.setToolTip(error_msg)
        widget_parent.setStyleSheet(
            """
    QFrame#{widgetname}{{
    border: 2px solid red;
    }}
        """.format(widgetname=widget_parent.objectName()))

        self.error_widgets.append(widget_parent)

    def highlight_tab(self, widget):
        pass
    #         widget_parent = widget.parent()
    #
    #     while not widget_parent.
    #         widget_parent = widget_parent.parent()
    #
    #     error_msg = "'Validation error in hidden contents, click to show'"
    #     widget_parent.setToolTip(error_msg)
    #     widget_parent.setStyleSheet(
    #         """
    # QTab#{widgetname}{{
    # border: 2px solid red;
    # }}
    #     """.format(widgetname=widget_parent.objectName()))
    #
    #     self.error_widgets.append(widget_parent

    def preview(self):
        """
        Shows a preview window with the xml content rendered using stylesheet

        Returns
        -------
        None
        """

        xsl_fname = utils.get_resource_path("FGDC/FGDC_Stylesheet.xsl")
        transform = etree.XSLT(etree.parse(xsl_fname))
        result = transform(self.metadata_root._to_xml())

        tmp = tempfile.NamedTemporaryFile(suffix='.html')
        tmp.close()
        result.write(tmp.name)

        self.preview = Preview(url=tmp.name)

        self.preview_dialog = QDialog(self)
        self.preview_dialog.setWindowTitle('Metadata Preview')
        self.preview_dialog.setLayout(self.preview.layout())

        self.preview_dialog.exec_()

    def launch_jupyter(self):
        """
        Launches a jupyter notebook server in our examples directory

        Returns
        -------
        None
        """
        from subprocess import Popen

        jupyter_dialog = JupyterLocationDialog()
        utils.set_window_icon(jupyter_dialog.msgBox)
        jupyter_dialog.msgBox.setWindowTitle("Where do you want to launch Jupyter?")
        ret = jupyter_dialog.msgBox.exec_()

        install_dir = utils.get_install_dname()
        if ret == 0:
            jupyter_dname = os.path.join(install_dir, 'examples')
        elif ret == 1:
            settings = QSettings('USGS', 'pymdwizard')
            last_jupyter_dname = settings.value('last_jupyter_dname')

            if last_jupyter_dname is None:
                last_jupyter_dname = os.path.join(install_dir, 'examples')
            jupyter_dname = QFileDialog.getExistingDirectory(self, "Select Directory to launch Jupyter from", last_jupyter_dname)
            if jupyter_dname:
                settings.setValue('last_jupyter_dname', jupyter_dname)
        else:
            return

        root_dir = os.path.dirname(install_dir)
        jupyterexe = os.path.join(root_dir, "Python35_64", "scripts", "jupyter.exe")

        if os.path.exists(jupyterexe) and os.path.exists(root_dir):
            p = Popen([jupyterexe, 'notebook'], cwd=jupyter_dname)

            msg = 'Jupyter launching...\nJupyter will start momentarily in a new tab in your default internet browser.'

            QMessageBox.information(self, "Launching Jupyter", msg)

    def update_from_github(self):
        from subprocess import check_output

        install_dir = utils.get_install_dname()
        root_dir = os.path.dirname(install_dir)
        update_bat = os.path.join(root_dir, 'update_wizard.bat')
        if os.path.exists(update_bat) and os.path.exists(root_dir):
            try:
                QApplication.setOverrideCursor(Qt.WaitCursor)
                p = check_output([update_bat], cwd=root_dir, shell=False)
                if p.splitlines()[-1] == b'Already up-to-date.':
                    msg = 'Application already up to date.'
                else:
                    msg = 'Application updated.\n\n'
                    msg += 'Please close and restart for these updates to take effect'
                QApplication.restoreOverrideCursor()
            except BaseException as e:
                import traceback
                msg = "Could not update application:\n{}".format(traceback.format_exc())
                QMessageBox.warning(self, "Recent Files", msg)

        else:
            msg = 'Could not find the batch file to update the application'

        QMessageBox.information(self, "Update results", msg)
Пример #4
0
class PyMdWizardMainForm(QMainWindow):

    max_recent_files = 5

    def __init__(self, parent=None):
        super(self.__class__, self).__init__()

        self.cur_fname = ''

        self.recent_file_actions = []
        self.error_widgets = []

        self.build_ui()
        self.connect_events()

    def build_ui(self):
        """
        Build and modify this widget's GUI

        Returns
        -------
        None
        """
        self.ui = UI_MainWindow.Ui_MainWindow()
        self.ui.setupUi(self)

        self.icon = QIcon(utils.get_resource_path('icons/Ducky.ico'))
        self.setWindowIcon(self.icon)

        self.metadata_root = MetadataRoot()
        self.ui.centralwidget.layout().addWidget(self.metadata_root)

        for i in range(PyMdWizardMainForm.max_recent_files):
            self.recent_file_actions.append(
                QAction(self, visible=False,
                        triggered=self.open_recent_file))
            self.ui.menuRecent_Files.addAction(self.recent_file_actions[i])
        self.update_recent_file_actions()

    def connect_events(self):
        """
        Connect the appropriate GUI components with the corresponding functions

        Returns
        -------
        None
        """
        self.ui.actionOpen.triggered.connect(self.open_file)
        self.ui.actionSave.triggered.connect(self.save_file)
        self.ui.actionSave_as.triggered.connect(self.save_as)
        self.ui.actionRun_Validation.triggered.connect(self.validate)
        self.ui.actionClear_validation.triggered.connect(self.clear_validation)
        self.ui.actionPreview.triggered.connect(self.preview)
        # self.ui.actionPull_From_Data.triggered.connect(self.harvest)

    def open_recent_file(self):
        """
        handles the opening of a recent file selection
        Returns
        -------
        None
        """
        action = self.sender()
        if action:
            self.load_file(action.data())

    def open_file(self):
        """
        Browse to a file and load it if it is acceptable

        Returns
        -------
        None
        """

        settings = QSettings('USGS', 'pymdwizard')
        recent_files = settings.value('recentFileList', [])
        if recent_files:
            dname, fname = os.path.split(recent_files[0])
        else:
            fname, dname = "", ""

        fname = QFileDialog.getOpenFileName(self, fname, dname, \
                                            filter="XML Files (*.xml)")


        if fname[0]:
            self.load_file(fname[0])
            self.update_recent_file_actions()

    def load_file(self, fname):
        """
        load a file's content into the application.

        Parameters
        ----------
        fname : str
                full file path and name of the file to load
        Returns
        -------
        None
        """
        self.clear_validation()

        file = QFile(fname)
        if not file.open(QFile.ReadWrite | QFile.Text):
            msg = "Cannot open file %s:\n%s." % (fname, file.errorString())
            QMessageBox.warning(self, "Recent Files", msg)

            return
        file.close()

        QApplication.setOverrideCursor(Qt.WaitCursor)
        QApplication.processEvents()
        exc_info = sys.exc_info()
        try:
            new_record = etree.parse(fname)
            self.metadata_root._from_xml(new_record)

            self.set_current_file(fname)
            self.statusBar().showMessage("File loaded", 2000)
        except BaseException as e:
            import traceback
            msg = "Cannot open file %s:\n%s." % (fname, traceback.format_exc())
            QMessageBox.warning(self, "Recent Files", msg)
        QApplication.restoreOverrideCursor()


    def save_as(self):
        """
        Navigate to a new or existing file and save the current document
        into this file.

        Returns
        -------
        None
        """
        fname = self.get_save_name()
        if fname:
            self.set_current_file(fname)
            self.update_recent_file_actions()
            self.save_file()

    def get_save_name(self):
        settings = QSettings('USGS', 'pymdwizard')
        recent_files = settings.value('recentFileList', [])
        if recent_files:
            dname, fname = os.path.split(recent_files[0])
        else:
            fname, dname = "", ""

        fname = QFileDialog.getSaveFileName(self, "Save As", dname, \
                                            filter="XML Files (*.xml)")
        return fname[0]


    def save_file(self, e=None):
        if not self.cur_fname:
            fname = self.get_save_name()
            if not fname:
                return
        else:
            fname = self.cur_fname

        fname_msg = utils.check_fname(fname)
        if not fname_msg == 'good':
            msg = "Cannot write to :\n  {}.".format(fname)
            QMessageBox.warning(self, "Metadata Wizard", msg)
            return

        xml_utils.save_to_file(self.metadata_root._to_xml(), fname)

        self.set_current_file(fname)
        self.statusBar().showMessage("File saved", 2000)

    def set_current_file(self, fname):
        self.cur_fname = fname
        if fname:
            title = "Metadata Wizard - {}".format(self.stripped_name(fname))
            self.setWindowTitle(title)
        else:
            self.setWindowTitle("Metadata Wizard")

        settings = QSettings('USGS', 'pymdwizard')
        files = settings.value('recentFileList', [])

        try:
            files.remove(fname)
        except ValueError:
            pass

        files.insert(0, fname)
        del files[PyMdWizardMainForm.max_recent_files:]

        settings.setValue('recentFileList', files)

        for widget in QApplication.topLevelWidgets():
            if isinstance(widget, PyMdWizardMainForm):
                widget.update_recent_file_actions()

    def update_recent_file_actions(self):
        settings = QSettings('USGS', 'pymdwizard')
        files = settings.value('recentFileList', [])

        num_recent_files = min(len(files), PyMdWizardMainForm.max_recent_files)

        for i in range(num_recent_files):
            text = "&%d %s" % (i + 1, self.stripped_name(files[i]))
            self.recent_file_actions[i].setText(text)
            self.recent_file_actions[i].setData(files[i])
            self.recent_file_actions[i].setVisible(True)

        for j in range(num_recent_files, PyMdWizardMainForm.max_recent_files):
            self.recent_file_actions[j].setVisible(False)

    def stripped_name(self, full_fname):
        return QFileInfo(full_fname).fileName()

    def clear_validation(self):

        annotation_lookup_fname = utils.get_resource_path("FGDC/bdp_lookup")
        with open(annotation_lookup_fname, encoding='utf-8') as data_file:
            annotation_lookup = json.loads(data_file.read())

        for widget in self.error_widgets:

            if not sip.isdeleted(widget) and \
                    widget.objectName() not in ['metadata_root', 'fgdc_metadata']:
                widget.setStyleSheet("""""")
                shortname = widget.objectName().replace('fgdc_', '')
                if shortname[-1].isdigit():
                    shortname = shortname[:-1]
                widget.setToolTip(annotation_lookup[shortname]['annotation'])

        self.error_widgets = []

    def validate(self):

        if self.metadata_root.schema == 'bdp':
            xsl_fname = utils.get_resource_path('FGDC/BDPfgdc-std-001-1998-annotated.xsd')
        else:
            xsl_fname = utils.get_resource_path('FGDC/fgdc-std-001-1998-annotated.xsd')
        from pymdwizard.core import fgdc_utils
        errors = fgdc_utils.validate_xml(self.metadata_root._to_xml(), xsl_fname)

        self.clear_validation()

        for error in errors:
            xpath, error_msg, line_num = error
            widget = self.metadata_root.get_widget(xpath)
            self.error_widgets.append(widget)

            if widget.objectName() not in ['metadata_root', 'fgdc_metadata']:
                widget.setToolTip(error_msg)
                widget.setStyleSheet(
                        """
QGroupBox#{widgetname}{{
  background-color: rgb(255,76,77);
    border: 2px solid red;
     subcontrol-position: top left; /* position at the top left*/
     padding-top: 20px;
    font: bold 14px;
    color: rgb(90, 90, 90);
 }}
QGroupBox#{widgetname}::title {{
text-align: left;
subcontrol-origin: padding;
subcontrol-position: top left; /* position at the top center */padding: 3 3px;
}}
QLabel{{
font: 9pt "Arial";
color: rgb(90, 90, 90);
}}
QLineEdit#{widgetname}, QPlainTextEdit#{widgetname}, QComboBox#{widgetname} {{
font: 9pt "Arial";
color: rgb(50, 50, 50);
background-color: rgb(255,76,77);
opacity: 25;
 }}
 QToolTip {{
    background-color: rgb(255,76,77);
    border-color: red;
    opacity: 255;
}}
                    """.format(widgetname=widget.objectName()))


    def preview(self):
        """
        Shows a preview window with the xml content rendered using stylesheet

        Returns
        -------
        None
        """

        xsl_fname = utils.get_resource_path("FGDC/FGDC_Stylesheet.xsl")
        transform = etree.XSLT(etree.parse(xsl_fname))
        result = transform(self.metadata_root._to_xml())

        tmp = tempfile.NamedTemporaryFile(suffix='.html')
        tmp.close()
        result.write(tmp.name)

        self.preview = Preview(url=tmp.name)

        self.preview_dialog = QDialog(self)
        self.preview_dialog.setWindowTitle('Metadata Preview')
        self.preview_dialog.setLayout(self.preview.layout())

        self.preview_dialog.exec_()