From d09bfdd09ba7719ae80a5af4e67bbbfaee7416f4 Mon Sep 17 00:00:00 2001 From: Chris PeBenito Date: Thu, 14 Apr 2016 10:03:34 -0400 Subject: [PATCH] apol: implement default query tab Closes #121 --- data/defaultquery.ui | 643 ++++++++++++++++++++++++++++++ setoolsgui/apol/chooseanalysis.py | 4 +- setoolsgui/apol/defaultquery.py | 171 ++++++++ setoolsgui/defaultmodel.py | 52 +++ 4 files changed, 869 insertions(+), 1 deletion(-) create mode 100644 data/defaultquery.ui create mode 100644 setoolsgui/apol/defaultquery.py create mode 100644 setoolsgui/defaultmodel.py diff --git a/data/defaultquery.ui b/data/defaultquery.ui new file mode 100644 index 0000000..233fb78 --- /dev/null +++ b/data/defaultquery.ui @@ -0,0 +1,643 @@ + + + DefaultQueryTab_ui + + + + 0 + 0 + 774 + 846 + + + + QAbstractScrollArea::AdjustToContents + + + true + + + + + 0 + 0 + 772 + 844 + + + + + 0 + 0 + + + + + 6 + + + 6 + + + 6 + + + 6 + + + 3 + + + + + Show or hide the search criteria (no settings are lost) + + + Criteria + + + true + + + + + + + + 0 + 0 + + + + + 16777215 + 20 + + + + + 12 + 75 + true + + + + Default Query + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 1 + + + + + 0 + 80 + + + + Optionally enter notes here about the query. + + + Enter notes here. + + + + + + + Show or hide the notes field (no data is lost) + + + Notes + + + + + + + + 0 + 2 + + + + 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 + + + + + + + + + + + + 0 + 0 + + + + Show: + + + + + + + + 0 + 1 + + + + + 16777215 + 16777215 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 0 + 0 + + + + + 16777215 + 150 + + + + Rule Type + + + + 6 + + + 6 + + + 6 + + + 6 + + + 3 + + + + + Clear + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Select All + + + + + + + true + + + default_user + + + true + + + + + + + default_role + + + true + + + + + + + default_type + + + true + + + + + + + default_range + + + true + + + + + + + + + + QDialogButtonBox::Apply + + + + + + + Object Class + + + + 6 + + + 6 + + + 6 + + + 6 + + + 3 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Clear + + + + + + + Invert + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + 0 + 0 + + + + + 250 + 16777215 + + + + Match the object class of the rule. + + + QAbstractItemView::ExtendedSelection + + + + + + + + + + + 16777215 + 100 + + + + Default + + + + 6 + + + 6 + + + 6 + + + 6 + + + 3 + + + + + + 0 + 0 + + + + + + + + + + low + + + + + high + + + + + low_high + + + + + + + + + 0 + 0 + + + + + + + + + + source + + + + + target + + + + + + + + + + + + + + + criteria_expander + notes_expander + default_user + default_type + default_role + default_range + clear_ruletypes + all_ruletypes + tclass + clear_class + invert_class + default_2 + default_range_2 + results_frame + table_results + raw_results + notes + + + + + criteria_expander + toggled(bool) + criteria_frame + setVisible(bool) + + + 629 + 16 + + + 379 + 239 + + + + + notes_expander + toggled(bool) + notes + setVisible(bool) + + + 735 + 16 + + + 386 + 776 + + + + + clear_class + clicked() + tclass + clearSelection() + + + 318 + 164 + + + 150 + 188 + + + + + diff --git a/setoolsgui/apol/chooseanalysis.py b/setoolsgui/apol/chooseanalysis.py index 0453e7f..a88366d 100644 --- a/setoolsgui/apol/chooseanalysis.py +++ b/setoolsgui/apol/chooseanalysis.py @@ -28,6 +28,7 @@ from .boundsquery import BoundsQueryTab from .categoryquery import CategoryQueryTab from .commonquery import CommonQueryTab from .constraintquery import ConstraintQueryTab +from .defaultquery import DefaultQueryTab from .dta import DomainTransitionAnalysisTab from .fsusequery import FSUseQueryTab from .genfsconquery import GenfsconQueryTab @@ -87,7 +88,8 @@ class ChooseAnalysis(SEToolsWidget, QDialog): "Nodecon Statements": NodeconQueryTab, "Portcon Statements": PortconQueryTab} general_choices = {"Summary": SummaryTab} - other_choices = {"Bounds": BoundsQueryTab} + other_choices = {"Bounds": BoundsQueryTab, + "Defaults": DefaultQueryTab} analysis_choices = {"Components": components_map, "Rules": rule_map, "Analyses": analysis_map, diff --git a/setoolsgui/apol/defaultquery.py b/setoolsgui/apol/defaultquery.py new file mode 100644 index 0000000..474d217 --- /dev/null +++ b/setoolsgui/apol/defaultquery.py @@ -0,0 +1,171 @@ +# 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 DefaultQuery + +from ..logtosignal import LogHandlerToSignal +from ..models import SEToolsListModel, invert_list_selection +from ..defaultmodel import DefaultTableModel +from ..widget import SEToolsWidget +from .queryupdater import QueryResultsUpdater + + +class DefaultQueryTab(SEToolsWidget, QScrollArea): + + """Default browser and query tab.""" + + def __init__(self, parent, policy, perm_map): + super(DefaultQueryTab, self).__init__(parent) + self.log = logging.getLogger(__name__) + self.policy = policy + self.query = DefaultQuery(policy) + self.setupUi() + + def __del__(self): + self.thread.quit() + self.thread.wait(5000) + logging.getLogger("setools.defaultquery").removeHandler(self.handler) + + def setupUi(self): + self.load_ui("defaultquery.ui") + + # set up results + self.table_results_model = DefaultTableModel(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(1, Qt.AscendingOrder) + + # populate class list + self.class_model = SEToolsListModel(self) + self.class_model.item_list = sorted(self.policy.classes()) + self.tclass.setModel(self.class_model) + + # 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.defaultquery").addHandler(self.handler) + + # Ensure settings are consistent with the initial .ui state + self.default_range_2.setEnabled(self.default_range.isChecked()) + self.notes.setHidden(not self.notes_expander.isChecked()) + + # connect signals + self.default_range.toggled.connect(self.default_range_2.setEnabled) + self.clear_ruletypes.clicked.connect(self.clear_all_ruletypes) + self.all_ruletypes.clicked.connect(self.set_all_ruletypes) + self.tclass.selectionModel().selectionChanged.connect(self.set_tclass) + self.invert_class.clicked.connect(self.invert_tclass_selection) + self.buttonBox.clicked.connect(self.run) + + # + # Ruletype criteria + # + def _set_ruletypes(self, value): + self.default_user.setChecked(value) + self.default_role.setChecked(value) + self.default_type.setChecked(value) + self.default_range.setChecked(value) + + def set_all_ruletypes(self): + self._set_ruletypes(True) + + def clear_all_ruletypes(self): + self._set_ruletypes(False) + + # + # Class criteria + # + def set_tclass(self): + selected_classes = [] + for index in self.tclass.selectionModel().selectedIndexes(): + selected_classes.append(self.class_model.data(index, Qt.UserRole)) + + self.query.tclass = selected_classes + + def invert_tclass_selection(self): + invert_list_selection(self.tclass.selectionModel()) + + # + # Results runner + # + def run(self, button): + # right now there is only one button. + rule_types = [] + + for mode in [self.default_user, self.default_role, self.default_type, self.default_range]: + if mode.isChecked(): + rule_types.append(mode.objectName()) + + self.query.ruletype = rule_types + self.query.default = self.default_2.currentData(Qt.DisplayRole) + + if self.default_range_2.isEnabled(): + self.query.default_range = self.default_range_2.currentData(Qt.DisplayRole) + else: + self.query.default_range = None + + # 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} default(s) 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 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/defaultmodel.py b/setoolsgui/defaultmodel.py new file mode 100644 index 0000000..e227208 --- /dev/null +++ b/setoolsgui/defaultmodel.py @@ -0,0 +1,52 @@ +# 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 .models import SEToolsTableModel + + +class DefaultTableModel(SEToolsTableModel): + + """Table-based model for default_*.""" + + headers = defaultdict(str, {0: "Rule Type", 1: "Class", 2: "Default", 3: "Default Range"}) + + 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 item.ruletype + elif col == 1: + return str(item.tclass) + elif col == 2: + return str(item.default) + elif col == 3: + try: + return str(item.default_range) + except AttributeError: + pass + + elif role == Qt.UserRole: + return item