Index: /trunk/src/VBox/ValidationKit/testmanager/core/report.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/core/report.py	(revision 65053)
+++ /trunk/src/VBox/ValidationKit/testmanager/core/report.py	(revision 65054)
@@ -38,5 +38,16 @@
 from testmanager.core.testcase      import TestCaseLogic;
 from testmanager.core.testcaseargs  import TestCaseArgsLogic;
+from testmanager.core.testresults   import TestResultLogic, TestResultFilter;
 from common                         import constants;
+
+
+
+class ReportFilter(TestResultFilter):
+    """
+    Same as TestResultFilter for now.
+    """
+
+    def __init__(self):
+        TestResultFilter.__init__(self);
 
 
@@ -73,5 +84,5 @@
 
 
-    def __init__(self, oDb, tsNow, cPeriods, cHoursPerPeriod, sSubject, aidSubjects):
+    def __init__(self, oDb, tsNow, cPeriods, cHoursPerPeriod, sSubject, aidSubjects, oFilter):
         ModelLogicBase.__init__(self, oDb);
         # Public so the report generator can easily access them.
@@ -82,12 +93,11 @@
         self.sSubject        = sSubject;
         self.aidSubjects     = aidSubjects;
+        self.oFilter         = oFilter;
 
     def getExtraSubjectTables(self):
         """
-        Returns a string with any extra tables needed by the subject. Each
-        table name is prefixed by a comma, so can be appended to a FROM
-        statement.
-        """
-        return '';
+        Returns a list of additional tables needed by the subject.
+        """
+        return [];
 
     def getExtraSubjectWhereExpr(self):
@@ -527,13 +537,16 @@
         """
 
+        sBaseQuery  = 'SELECT   TestSets.enmStatus, COUNT(TestSets.idTestSet)\n' \
+                      'FROM     TestSets\n' \
+                    + self.oFilter.getTableJoins();
+        for sTable in self.getExtraSubjectTables():
+            sBaseQuery = sBaseQuery[:-1] + ',\n         ' + sTable + '\n';
+        sBaseQuery += 'WHERE    enmStatus <> \'running\'\n' \
+                    + self.oFilter.getWhereConditions() \
+                    + self.getExtraSubjectWhereExpr();
+
         adPeriods = [];
         for iPeriod in xrange(self.cPeriods):
-            self._oDb.execute('SELECT   enmStatus, COUNT(TestSets.idTestSet)\n'
-                              'FROM     TestSets' + self.getExtraSubjectTables() +'\n'
-                              'WHERE    enmStatus <> \'running\'\n'
-                              + self.getExtraSubjectWhereExpr()
-                              + self.getExtraWhereExprForPeriod(iPeriod)
-                              +
-                              'GROUP BY enmStatus\n');
+            self._oDb.execute(sBaseQuery + self.getExtraWhereExprForPeriod(iPeriod) + 'GROUP BY enmStatus\n');
 
             dRet = \
@@ -567,32 +580,42 @@
         oFailureReasonLogic = FailureReasonLogic(self._oDb);
 
+        #
         # Create a temporary table
+        #
         sTsNow   = 'CURRENT_TIMESTAMP' if self.tsNow is None else self._oDb.formatBindArgs('%s::TIMESTAMP', (self.tsNow,));
         sTsFirst = '(%s - interval \'%s hours\')' \
                  % (sTsNow, self.cHoursPerPeriod * self.cPeriods,);
-        self._oDb.execute('CREATE TEMPORARY TABLE TmpReasons ON COMMIT DROP AS\n'
-                          'SELECT   TestResultFailures.idFailureReason AS idFailureReason,\n'
-                          '         TestResultFailures.idTestResult    AS idTestResult,\n'
-                          '         TestSets.idTestSet                 AS idTestSet,\n'
-                          '         TestSets.tsDone                    AS tsDone,\n'
-                          '         TestSets.tsCreated                 AS tsCreated,\n'
-                          '         TestSets.idBuild                   AS idBuild\n'
-                          'FROM     TestResultFailures,\n'
-                          '         TestResults,\n'
-                          '         TestSets' + self.getExtraSubjectTables() + '\n'
-                          'WHERE    TestResultFailures.idTestResult = TestResults.idTestResult\n'
-                          '     AND TestResultFailures.tsExpire     = \'infinity\'::TIMESTAMP\n'
-                          '     AND TestResultFailures.tsEffective >= ' + sTsFirst + '\n'
-                          '     AND TestResults.enmStatus          <> \'running\'\n'
-                          '     AND TestResults.enmStatus          <> \'success\'\n'
-                          '     AND TestResults.tsCreated          >= ' + sTsFirst + '\n'
-                          '     AND TestResults.tsCreated          <  ' + sTsNow + '\n'
-                          '     AND TestResults.idTestSet           = TestSets.idTestSet\n'
-                          '     AND TestSets.tsDone                >= ' + sTsFirst + '\n'
-                          '     AND TestSets.tsDone                <  ' + sTsNow + '\n'
-                          + self.getExtraSubjectWhereExpr());
+        sQuery   = 'CREATE TEMPORARY TABLE TmpReasons ON COMMIT DROP AS\n' \
+                   'SELECT   TestResultFailures.idFailureReason AS idFailureReason,\n' \
+                   '         TestResultFailures.idTestResult    AS idTestResult,\n' \
+                   '         TestSets.idTestSet                 AS idTestSet,\n' \
+                   '         TestSets.tsDone                    AS tsDone,\n' \
+                   '         TestSets.tsCreated                 AS tsCreated,\n' \
+                   '         TestSets.idBuild                   AS idBuild\n' \
+                   'FROM     TestResultFailures,\n' \
+                   '         TestResults,\n' \
+                   '         TestSets\n' \
+                   + self.oFilter.getTableJoins(dOmitTables = {'TestResults': True, 'TestResultFailures': True});
+        for sTable in self.getExtraSubjectTables():
+            if sTable not in [ 'TestResults', 'TestResultFailures' ] and not self.oFilter.isJoiningWithTable(sTable):
+                sQuery = sQuery[:-1] + ',\n         ' + sTable + '\n';
+        sQuery  += 'WHERE    TestResultFailures.idTestResult = TestResults.idTestResult\n' \
+                   '     AND TestResultFailures.tsExpire     = \'infinity\'::TIMESTAMP\n' \
+                   '     AND TestResultFailures.tsEffective >= ' + sTsFirst + '\n' \
+                   '     AND TestResults.enmStatus          <> \'running\'\n' \
+                   '     AND TestResults.enmStatus          <> \'success\'\n' \
+                   '     AND TestResults.tsCreated          >= ' + sTsFirst + '\n' \
+                   '     AND TestResults.tsCreated          <  ' + sTsNow + '\n' \
+                   '     AND TestResults.idTestSet           = TestSets.idTestSet\n' \
+                   '     AND TestSets.tsDone                >= ' + sTsFirst + '\n' \
+                   '     AND TestSets.tsDone                <  ' + sTsNow + '\n' \
+                   + self.oFilter.getWhereConditions() \
+                   + self.getExtraSubjectWhereExpr();
+        self._oDb.execute(sQuery);
         self._oDb.execute('SELECT idFailureReason FROM TmpReasons;');
 
+        #
         # Retrieve the period results.
+        #
         oSet = ReportFailureReasonSet();
         for iPeriod in xrange(self.cPeriods):
@@ -735,16 +758,21 @@
         oSet = ReportPeriodSetWithTotalBase(sIdColumn if sIdAttr is None else sIdAttr);
 
+        # Construct base query.
+        sBaseQuery  = 'SELECT   TestSets.' + sIdColumn + ',\n' \
+                      '         COUNT(CASE WHEN TestSets.enmStatus >= \'failure\' THEN 1 END),\n' \
+                      '         MIN(TestSets.tsDone),\n' \
+                      '         MAX(TestSets.tsDone),\n' \
+                      '         COUNT(TestSets.idTestResult)\n' \
+                      'FROM     TestSets\n' \
+                      + self.oFilter.getTableJoins();
+        for sTable in self.getExtraSubjectTables():
+            sBaseQuery = sBaseQuery[:-1] + ',\n         ' + sTable + '\n';
+        sBaseQuery += 'WHERE    TRUE\n' \
+                    + self.oFilter.getWhereConditions() \
+                    + self.getExtraSubjectWhereExpr() + '\n';
+
         # Retrieve the period results.
         for iPeriod in xrange(self.cPeriods):
-            self._oDb.execute('SELECT   ' + sIdColumn + ',\n'
-                              '         COUNT(CASE WHEN enmStatus >= \'failure\' THEN 1 END),\n'
-                              '         MIN(tsDone),\n'
-                              '         MAX(tsDone),\n'
-                              '         COUNT(idTestResult)\n'
-                              'FROM     TestSets' + self.getExtraSubjectTables() + '\n'
-                              'WHERE    TRUE\n'
-                              + self.getExtraWhereExprForPeriod(iPeriod)
-                              + self.getExtraSubjectWhereExpr() + '\n'
-                              'GROUP BY ' + sIdColumn + '\n');
+            self._oDb.execute(sBaseQuery + self.getExtraWhereExprForPeriod(iPeriod) + 'GROUP BY TestSets.' + sIdColumn + '\n');
             aaoRows = self._oDb.fetchAll()
 
@@ -797,25 +825,31 @@
         """
         sSorting = 'ASC' if fEnter else 'DESC';
-        self._oDb.execute('SELECT   TestSets.idTestResult,\n'
-                          '         TestSets.idTestSet,\n'
-                          '         TestSets.tsDone,\n'
-                          '         TestSets.idBuild,\n'
-                          '         Builds.iRevision,\n'
-                          '         BuildCategories.sRepository\n'
-                          'FROM     TestSets,\n'
-                          '         Builds,\n'
-                          '         BuildCategories' + self.getExtraSubjectTables() + '\n'
-                          'WHERE    TestSets.' + sIdColumn + '      = %s\n'
-                          '     AND TestSets.idBuild          = Builds.idBuild\n'
-                          '     AND TestSets.enmStatus       >= \'failure\'\n'
-                          + self.getExtraWhereExprForPeriod(iPeriod) +
-                          '     AND Builds.tsExpire           > TestSets.tsCreated\n'
-                          '     AND Builds.tsEffective       <= TestSets.tsCreated\n'
-                          '     AND Builds.idBuildCategory    = BuildCategories.idBuildCategory'
-                          + self.getExtraSubjectWhereExpr() + '\n'
-                          'ORDER BY Builds.iRevision ' + sSorting + ',\n'
-                          '         TestSets.tsCreated ' + sSorting + '\n'
-                          'LIMIT 1\n'
-                          , ( idSubject, ));
+        sQuery   = 'SELECT   TestSets.idTestResult,\n' \
+                   '         TestSets.idTestSet,\n' \
+                   '         TestSets.tsDone,\n' \
+                   '         TestSets.idBuild,\n' \
+                   '         Builds.iRevision,\n' \
+                   '         BuildCategories.sRepository\n' \
+                   'FROM     TestSets\n' \
+                   + self.oFilter.getTableJoins(dOmitTables = {'Builds': True, 'BuildCategories': True});
+        sQuery   = sQuery[:-1] + ',\n' \
+                   '         Builds,\n' \
+                   '         BuildCategories\n';
+        for sTable in self.getExtraSubjectTables():
+            if sTable not in [ 'Builds', 'BuildCategories' ] and not self.oFilter.isJoiningWithTable(sTable):
+                sQuery = sQuery[:-1] + ',\n         ' + sTable + '\n';
+        sQuery  += 'WHERE    TestSets.' + sIdColumn + '      = ' + str(idSubject) + '\n' \
+                   '     AND TestSets.idBuild          = Builds.idBuild\n' \
+                   '     AND TestSets.enmStatus       >= \'failure\'\n' \
+                   + self.getExtraWhereExprForPeriod(iPeriod) + \
+                   '     AND Builds.tsExpire           > TestSets.tsCreated\n' \
+                   '     AND Builds.tsEffective       <= TestSets.tsCreated\n' \
+                   '     AND Builds.idBuildCategory    = BuildCategories.idBuildCategory\n' \
+                   + self.oFilter.getWhereConditions() \
+                   + self.getExtraSubjectWhereExpr() + '\n' \
+                   'ORDER BY Builds.iRevision ' + sSorting + ',\n' \
+                   '         TestSets.tsCreated ' + sSorting + '\n' \
+                   'LIMIT 1\n';
+        self._oDb.execute(sQuery);
         aoRow = self._oDb.fetchOne();
         if aoRow is None:
@@ -826,5 +860,9 @@
                                    iPeriod = iPeriod, fEnter = fEnter, idSubject = idSubject, oSubject = oSubject);
 
-
+    def fetchPossibleFilterOptions(self, oFilter, tsNow, sPeriod):
+        """
+        Fetches possible filtering options.
+        """
+        return TestResultLogic(self._oDb).fetchPossibleFilterOptions(oFilter, tsNow, sPeriod);
 
 
Index: /trunk/src/VBox/ValidationKit/testmanager/core/testresults.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/core/testresults.py	(revision 65053)
+++ /trunk/src/VBox/ValidationKit/testmanager/core/testresults.py	(revision 65054)
@@ -660,15 +660,6 @@
     def __init__(self):
         ModelFilterBase.__init__(self);
-        oCrit = FilterCriterion('Test status', sVarNm = 'ts', sType = FilterCriterion.ksType_String,
+        oCrit = FilterCriterion('Test statuses', sVarNm = 'ts', sType = FilterCriterion.ksType_String,
                                 sTable = 'TestSets', sColumn = 'enmStatus');
-        oCrit.aoPossible = (
-            FilterCriterionValueAndDescription(TestResultData.ksTestStatus_Success,  'Success'),
-            FilterCriterionValueAndDescription(TestResultData.ksTestStatus_Running,  'Running'),
-            FilterCriterionValueAndDescription(TestResultData.ksTestStatus_Skipped,  'Skipped'),
-            FilterCriterionValueAndDescription(TestResultData.ksTestStatus_Aborted,  'Aborted'),
-            FilterCriterionValueAndDescription(TestResultData.ksTestStatus_Failure,  'Failure'),
-            FilterCriterionValueAndDescription(TestResultData.ksTestStatus_TimedOut, 'Timed out'),
-            FilterCriterionValueAndDescription(TestResultData.ksTestStatus_Rebooted, 'Rebooted'),
-        );
         self.aCriteria.append(oCrit);
         assert self.aCriteria[self.kiTestStatus] is oCrit;
@@ -695,9 +686,9 @@
         assert self.aCriteria[self.kiRevisions] is oCrit;
 
-        oCrit = FilterCriterion('CPU Arches', sVarNm = 'ca', sTable = 'TestBoxesWithStrings', sColumn = 'idStrCpuArch');
+        oCrit = FilterCriterion('CPU arches', sVarNm = 'ca', sTable = 'TestBoxesWithStrings', sColumn = 'idStrCpuArch');
         self.aCriteria.append(oCrit);
         assert self.aCriteria[self.kiCpuArches] is oCrit;
 
-        oCrit = FilterCriterion('CPU Vendor', sVarNm = 'cv', sTable = 'TestBoxesWithStrings', sColumn = 'idStrCpuVendor');
+        oCrit = FilterCriterion('CPU vendors', sVarNm = 'cv', sTable = 'TestBoxesWithStrings', sColumn = 'idStrCpuVendor');
         self.aCriteria.append(oCrit);
         assert self.aCriteria[self.kiCpuVendors] is oCrit;
@@ -711,5 +702,5 @@
         assert self.aCriteria[self.kiOsVersions] is oCrit;
 
-        oCrit = FilterCriterion('Failure Reasons', sVarNm = 'fr', sTable = 'TestResultFailures', sColumn = 'idFailureReason');
+        oCrit = FilterCriterion('Failure reasons', sVarNm = 'fr', sTable = 'TestResultFailures', sColumn = 'idFailureReason');
         self.aCriteria.append(oCrit);
         assert self.aCriteria[self.kiFailReasons] is oCrit;
@@ -731,11 +722,14 @@
         return sQuery;
 
-    def getTableJoins(self, sExtraIndent = '', iOmit = -1):
+    def getTableJoins(self, sExtraIndent = '', iOmit = -1, dOmitTables = None):
         """
         Construct the WHERE conditions for the filter, optionally omitting one
         criterion.
         """
+        afDone = { 'TestSets': True, };
+        if dOmitTables is not None:
+            afDone.update(dOmitTables);
+
         sQuery = '';
-        afDone = { 'TestSets': True, };
         for iCrit, oCrit in enumerate(self.aCriteria):
             if    oCrit.sState == FilterCriterion.ksState_Selected \
@@ -1455,4 +1449,5 @@
             """ Does the tedious result fetching and handling of missing bits. """
             dLeft = { oValue: 1 for oValue in oCrit.aoSelected };
+            oCrit.aoPossible = [];
             for aoRow in self._oDb.fetchAll():
                 oCrit.aoPossible.append(FilterCriterionValueAndDescription(aoRow[0], aoRow[1]));
@@ -1471,4 +1466,14 @@
                                                                                        getattr(oMissing, sNameAttr),
                                                                                        fIrrelevant = True));
+
+        # Statuses.
+        oCrit = oFilter.aCriteria[TestResultFilter.kiTestStatus];
+        self._oDb.execute('SELECT TestSets.enmStatus, TestSets.enmStatus, COUNT(TestSets.idTestSet)\n'
+                          'FROM   TestSets\n' + oFilter.getTableJoins(iOmit = TestResultFilter.kiTestStatus) +
+                          'WHERE  ' + self._getTimePeriodQueryPart(tsNow, sPeriod) +
+                          oFilter.getWhereConditions(iOmit = TestResultFilter.kiTestStatus) +
+                          'GROUP BY TestSets.enmStatus\n'
+                          'ORDER BY TestSets.enmStatus\n');
+        workerDoFetch(None, fIdIsName = True);
 
         # Scheduling groups (see getSchedGroups).
@@ -1599,5 +1604,5 @@
                           '               ON BuildCategories.idBuildCategory = BuildCategoryIDs.idBuildCategory\n'
                           'ORDER BY BuildCategories.sBranch DESC\n' );
-        workerDoFetch(BuildCategoryLogic, fIdIsName = True);
+        workerDoFetch(None, fIdIsName = True);
 
         # Failure reasons.
Index: /trunk/src/VBox/ValidationKit/testmanager/webui/wuimain.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/webui/wuimain.py	(revision 65053)
+++ /trunk/src/VBox/ValidationKit/testmanager/webui/wuimain.py	(revision 65054)
@@ -901,6 +901,6 @@
         return True;
 
-    def _generateResultFilter(self, oFilter, oResultLogic, tsNow, sPeriod, enmResultsGroupingType, aoGroupMembers,
-                              fOnlyFailures, fOnlyNeedingReason):
+    def _generateResultFilter(self, oFilter, oResultLogic, tsNow, sPeriod, enmResultsGroupingType = None, aoGroupMembers = None,
+                              fOnlyFailures = False, fOnlyNeedingReason = False):
         """
         Generates the result filter for the left hand side.
@@ -927,5 +927,5 @@
                  u' <dl>\n';
 
-        for iCrit, oCrit in enumerate(oFilter.aCriteria):
+        for oCrit in oFilter.aCriteria:
             if len(oCrit.aoPossible) > 0:
                 sClass = 'sf-collapsable' if oCrit.sState == oCrit.ksState_Selected else 'sf-expandable';
@@ -1188,8 +1188,9 @@
         return self.ksDispatchRcAllDone;
 
-    def _actionGenericReport(self, oModelType, oReportType):
+    def _actionGenericReport(self, oModelType, oFilterType, oReportType):
         """
         Generic report action.
         oReportType is a child of WuiReportContentBase.
+        oFilterType is a child of ModelFilterBase.
         oModelType is a child of ReportModelBase.
         """
@@ -1207,4 +1208,5 @@
             if aidSubjects is None:
                 raise WuiException('Missing parameter %s' % (self.ksParamReportSubjectIds,));
+        oFilter = oFilterType().initFromParams(self);
         self._checkForUnknownParameters();
 
@@ -1217,35 +1219,38 @@
             self.ksParamReportSubjectIds:       aidSubjects,
         };
-
-        oModel   = oModelType(self._oDb, tsEffective, cPeriods, cHoursPerPeriod, sSubject, aidSubjects);
+        ## @todo oFilter.
+
+        oModel   = oModelType(self._oDb, tsEffective, cPeriods, cHoursPerPeriod, sSubject, aidSubjects, oFilter);
         oContent = oReportType(oModel, dParams, fSubReport = False, fnDPrint = self._oSrvGlue.dprint, oDisp = self);
         (self._sPageTitle, self._sPageBody) = oContent.show();
         sNavi = self._generateReportNavigation(tsEffective, cHoursPerPeriod, cPeriods);
         self._sPageBody = sNavi + self._sPageBody;
+
+        self._sPageFilter = self._generateResultFilter(oFilter, oModel, tsEffective, '%s hours' % (cHoursPerPeriod * cPeriods,));
         return True;
 
     def _actionReportSummary(self):
         """ Action wrapper. """
-        from testmanager.core.report                import ReportLazyModel;
+        from testmanager.core.report                import ReportLazyModel, ReportFilter;
         from testmanager.webui.wuireport            import WuiReportSummary;
-        return self._actionGenericReport(ReportLazyModel, WuiReportSummary);
+        return self._actionGenericReport(ReportLazyModel, ReportFilter, WuiReportSummary);
 
     def _actionReportRate(self):
         """ Action wrapper. """
-        from testmanager.core.report                import ReportLazyModel;
+        from testmanager.core.report                import ReportLazyModel, ReportFilter;
         from testmanager.webui.wuireport            import WuiReportSuccessRate;
-        return self._actionGenericReport(ReportLazyModel, WuiReportSuccessRate);
+        return self._actionGenericReport(ReportLazyModel, ReportFilter, WuiReportSuccessRate);
 
     def _actionReportTestCaseFailures(self):
         """ Action wrapper. """
-        from testmanager.core.report                import ReportLazyModel;
+        from testmanager.core.report                import ReportLazyModel, ReportFilter;
         from testmanager.webui.wuireport            import WuiReportTestCaseFailures;
-        return self._actionGenericReport(ReportLazyModel, WuiReportTestCaseFailures);
+        return self._actionGenericReport(ReportLazyModel, ReportFilter, WuiReportTestCaseFailures);
 
     def _actionReportFailureReasons(self):
         """ Action wrapper. """
-        from testmanager.core.report                import ReportLazyModel;
+        from testmanager.core.report                import ReportLazyModel, ReportFilter;
         from testmanager.webui.wuireport            import WuiReportFailureReasons;
-        return self._actionGenericReport(ReportLazyModel, WuiReportFailureReasons);
+        return self._actionGenericReport(ReportLazyModel, ReportFilter, WuiReportFailureReasons);
 
     def _actionGraphWiz(self):
