diff --git a/data/categoryquery.ui b/data/categoryquery.ui new file mode 100644 index 0000000..832d661 --- /dev/null +++ b/data/categoryquery.ui @@ -0,0 +1,412 @@ + + + CategoryQueryTab_ui + + + + 0 + 0 + 774 + 846 + + + + QAbstractScrollArea::AdjustToContents + + + true + + + + + 0 + 0 + 772 + 844 + + + + + 0 + 0 + + + + + + + + 0 + 0 + + + + + 16777215 + 20 + + + + + 11 + 75 + true + + + + Categories + + + + + + + + 0 + 80 + + + + Optionally enter notes here about the query. + + + Enter notes here. + + + + + + + Qt::Horizontal + + + + 440 + 20 + + + + + + + + Show: + + + + + + + Show or hide the search criteria (no settings are lost) + + + Criteria + + + true + + + + + + + Show or hide the notes field (no data is lost) + + + Notes + + + + + + + + 0 + 1 + + + + Qt::Horizontal + + + + Categories Browser + + + + + + + + + + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Search Criteria + + + + + + QDialogButtonBox::Apply + + + + + + + + 16777215 + 120 + + + + Category Name + + + + 6 + + + 6 + + + 6 + + + 6 + + + 3 + + + + + + 0 + 0 + + + + + 150 + 20 + + + + + 250 + 16777215 + + + + + + + + Use regular expressions to match the type's name. + + + Regex + + + + + + + + + + + + + + 0 + 1 + + + + 0 + + + + + 0 + 0 + + + + Results + + + + 6 + + + 6 + + + 6 + + + 6 + + + + + + 0 + 0 + + + + QAbstractScrollArea::AdjustIgnored + + + true + + + true + + + + + + + + + 0 + 0 + + + + Raw Results + + + + 6 + + + 6 + + + 6 + + + 6 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + Monospace + + + + + + + QPlainTextEdit::NoWrap + + + true + + + + + + + + + + + + + splitter + notes + label + label_2 + horizontalSpacer + criteria_expander + notes_expander + + + + + GetDetailsListView + QListView +
setoolsgui/getdetailslist.h
+
+
+ + criteria_expander + notes_expander + cats + name + name_regex + results_frame + table_results + raw_results + notes + + + + + notes_expander + toggled(bool) + notes + setVisible(bool) + + + 732 + 20 + + + 386 + 754 + + + + + criteria_expander + toggled(bool) + criteria_frame + setVisible(bool) + + + 583 + 20 + + + 496 + 226 + + + + +
diff --git a/setoolsgui/apol/categoryquery.py b/setoolsgui/apol/categoryquery.py new file mode 100644 index 0000000..a19a0bf --- /dev/null +++ b/setoolsgui/apol/categoryquery.py @@ -0,0 +1,175 @@ +# Copyright 2016, Tresys Technology, LLC +# +# This file is part of SETools. +# +# SETools is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation, either version 2.1 of +# the License, or (at your option) any later version. +# +# SETools is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with SETools. If not, see +# . +# + +import logging + +from PyQt5.QtCore import Qt, QSortFilterProxyModel, QStringListModel, QThread +from PyQt5.QtGui import QPalette, QTextCursor +from PyQt5.QtWidgets import QCompleter, QHeaderView, QMessageBox, QProgressDialog, QScrollArea +from setools import CategoryQuery + +from ..logtosignal import LogHandlerToSignal +from ..models import SEToolsListModel, invert_list_selection +from ..mlsmodel import MLSComponentTableModel, category_detail +from ..widget import SEToolsWidget +from .queryupdater import QueryResultsUpdater + + +class CategoryQueryTab(SEToolsWidget, QScrollArea): + + """Category browser and query tab.""" + + def __init__(self, parent, policy, perm_map): + super(CategoryQueryTab, self).__init__(parent) + self.log = logging.getLogger(__name__) + self.policy = policy + self.query = CategoryQuery(policy) + self.setupUi() + + def __del__(self): + self.thread.quit() + self.thread.wait(5000) + logging.getLogger("setools.categoryquery").removeHandler(self.handler) + + def setupUi(self): + self.load_ui("categoryquery.ui") + + # populate category list + self.category_model = SEToolsListModel(self) + self.category_model.item_list = sorted(self.policy.categories()) + self.cats.setModel(self.category_model) + + # set up results + self.table_results_model = MLSComponentTableModel(self) + self.sort_proxy = QSortFilterProxyModel(self) + self.sort_proxy.setSourceModel(self.table_results_model) + self.table_results.setModel(self.sort_proxy) + self.table_results.sortByColumn(0, Qt.AscendingOrder) + + # setup indications of errors on level/range + self.orig_palette = self.name.palette() + self.error_palette = self.name.palette() + self.error_palette.setColor(QPalette.Base, Qt.red) + self.clear_name_error() + + # set up processing thread + self.thread = QThread() + self.worker = QueryResultsUpdater(self.query, self.table_results_model) + self.worker.moveToThread(self.thread) + self.worker.raw_line.connect(self.raw_results.appendPlainText) + self.worker.finished.connect(self.update_complete) + self.worker.finished.connect(self.thread.quit) + self.thread.started.connect(self.worker.update) + + # create a "busy, please wait" dialog + self.busy = QProgressDialog(self) + self.busy.setModal(True) + self.busy.setRange(0, 0) + self.busy.setMinimumDuration(0) + self.busy.canceled.connect(self.thread.requestInterruption) + self.busy.reset() + + # update busy dialog from query INFO logs + self.handler = LogHandlerToSignal() + self.handler.message.connect(self.busy.setLabelText) + logging.getLogger("setools.categoryquery").addHandler(self.handler) + + # Ensure settings are consistent with the initial .ui state + self.notes.setHidden(not self.notes_expander.isChecked()) + + # connect signals + self.cats.doubleClicked.connect(self.get_detail) + self.cats.get_detail.triggered.connect(self.get_detail) + self.name.textEdited.connect(self.clear_name_error) + self.name.editingFinished.connect(self.set_name) + self.name_regex.toggled.connect(self.set_name_regex) + self.buttonBox.clicked.connect(self.run) + + # + # Category browser + # + def get_detail(self): + # .ui is set for single item selection. + index = self.cats.selectedIndexes()[0] + item = self.category_model.data(index, Qt.UserRole) + + self.log.debug("Generating detail window for {0}".format(item)) + category_detail(self, item) + + # + # Name criteria + # + def clear_name_error(self): + self.name.setToolTip("Match the category name.") + self.name.setPalette(self.orig_palette) + + def set_name(self): + try: + self.query.name = self.name.text() + except Exception as ex: + self.log.error("Category name error: {0}".format(ex)) + self.name.setToolTip("Error: " + str(ex)) + self.name.setPalette(self.error_palette) + + def set_name_regex(self, state): + self.log.debug("Setting name_regex {0}".format(state)) + self.query.name_regex = state + self.clear_name_error() + self.set_name() + + # + # Results runner + # + + def run(self, button): + # right now there is only one button. + + # start processing + self.busy.setLabelText("Processing query...") + self.busy.show() + self.raw_results.clear() + self.thread.start() + + def update_complete(self, count): + self.log.info("{0} categories found.".format(count)) + + # update sizes/location of result displays + if not self.busy.wasCanceled(): + self.busy.setLabelText("Resizing the result table's columns; GUI may be unresponsive") + self.busy.repaint() + self.table_results.resizeColumnsToContents() + # If the attrs or alias column widths are too long, pull back + # to a reasonable size + header = self.table_results.horizontalHeader() + if header.sectionSize(1) > 400: + header.resizeSection(1, 400) + if header.sectionSize(2) > 400: + header.resizeSection(2, 400) + + if not self.busy.wasCanceled(): + self.busy.setLabelText("Resizing the result table's rows; GUI may be unresponsive") + self.busy.repaint() + self.table_results.resizeRowsToContents() + + if not self.busy.wasCanceled(): + self.busy.setLabelText("Moving the raw result to top; GUI may be unresponsive") + self.busy.repaint() + self.raw_results.moveCursor(QTextCursor.Start) + + self.busy.reset() diff --git a/setoolsgui/apol/chooseanalysis.py b/setoolsgui/apol/chooseanalysis.py index 182b848..a66a415 100644 --- a/setoolsgui/apol/chooseanalysis.py +++ b/setoolsgui/apol/chooseanalysis.py @@ -24,6 +24,7 @@ from ..widget import SEToolsWidget # Analysis tabs: from .boolquery import BoolQueryTab +from .categoryquery import CategoryQueryTab from .commonquery import CommonQueryTab from .constraintquery import ConstraintQueryTab from .dta import DomainTransitionAnalysisTab @@ -92,6 +93,7 @@ class ChooseAnalysis(SEToolsWidget, QDialog): if mls: rule_map["MLS Rules"] = MLSRuleQueryTab + components_map["Categories"] = CategoryQueryTab # populate the item list: self.analysisTypes.clear() diff --git a/setoolsgui/mlsmodel.py b/setoolsgui/mlsmodel.py new file mode 100644 index 0000000..d4c5861 --- /dev/null +++ b/setoolsgui/mlsmodel.py @@ -0,0 +1,77 @@ +# Copyright 2016, Tresys Technology, LLC +# +# This file is part of SETools. +# +# SETools is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation, either version 2.1 of +# the License, or (at your option) any later version. +# +# SETools is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with SETools. If not, see +# . +# +from collections import defaultdict + +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QPalette, QTextCursor + +from .details import DetailsPopup +from .models import SEToolsTableModel + + +def _mls_detail(parent, obj, objtype): + """ + Create a dialog box for category or sensitivity details. + + Parameters: + parent The parent Qt Widget + type_ The type + """ + + detail = DetailsPopup(parent, "{0} detail: {1}".format(objtype, obj)) + + aliases = sorted(obj.aliases()) + detail.append_header("Aliases ({0}):".format(len(aliases))) + for a in aliases: + detail.append(" {0}".format(a)) + + detail.show() + + +def category_detail(parent, obj): + """ + Create a dialog box for category details. + + Parameters: + parent The parent Qt Widget + type_ The type + """ + _mls_detail(parent, obj, "Category") + + +class MLSComponentTableModel(SEToolsTableModel): + + """Table-based model for sensitivities and categories.""" + + headers = defaultdict(str, {0: "Name", 1: "Aliases"}) + + def data(self, index, role): + if self.resultlist: + row = index.row() + col = index.column() + item = self.resultlist[row] + + if role == Qt.DisplayRole: + if col == 0: + return str(item) + elif col == 1: + return ", ".join(sorted(str(a) for a in item.aliases())) + + elif role == Qt.UserRole: + return item