Index: /trunk/src/VBox/ValidationKit/testmanager/core/testresults.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/core/testresults.py	(revision 61266)
+++ /trunk/src/VBox/ValidationKit/testmanager/core/testresults.py	(revision 61267)
@@ -445,5 +445,5 @@
     ksParam_sComment            = 'TestResultFailure_sComment';
 
-    kasAllowNullAttributes      = ['tsEffective', 'tsExpire', 'uidAuthor', 'sComment' ];
+    kasAllowNullAttributes      = ['tsEffective', 'tsExpire', 'uidAuthor', 'sComment', 'idTestSet' ];
 
     kcDbColumns                 = 7;
@@ -618,8 +618,8 @@
         self.oFailureReason          = None;
         if aoRow[31] is not None:
-            self.oFailureReason = oFailureReasonLogic.cachedLookup(aoRow[30]);
+            self.oFailureReason = oFailureReasonLogic.cachedLookup(aoRow[31]);
         self.oFailureReasonAssigner  = None;
         if aoRow[32] is not None:
-            self.oFailureReasonAssigner = oUserAccountLogic.cachedLookup(aoRow[31]);
+            self.oFailureReasonAssigner = oUserAccountLogic.cachedLookup(aoRow[32]);
         self.tsFailureReasonAssigned = aoRow[33];
         self.sFailureReasonComment   = aoRow[34];
@@ -769,12 +769,12 @@
     kdResultGroupingMap = {
         ksResultsGroupingTypeNone: (
-            # Grouping tables;     # Grouping field;          # Grouping where addition.  # Sort by overrides.
-            'TestSets',            None,                      None,                       {}
+            # Grouping tables;                # Grouping field;          # Grouping where addition.  # Sort by overrides.
+            '',                                None,                      None,                       {}
         ),
-        ksResultsGroupingTypeTestGroup:  ('TestSets',   'TestSets.idTestGroup',     None, {}),
-        ksResultsGroupingTypeTestBox:    ('TestSets',   'TestSets.idTestBox',       None, {}),
-        ksResultsGroupingTypeTestCase:   ('TestSets',   'TestSets.idTestCase',      None, {}),
+        ksResultsGroupingTypeTestGroup:  ('', 'TestSets.idTestGroup',     None,                {}),
+        ksResultsGroupingTypeTestBox:    ('', 'TestSets.idTestBox',       None,                {}),
+        ksResultsGroupingTypeTestCase:   ('', 'TestSets.idTestCase',      None,                {}),
         ksResultsGroupingTypeBuildRev: (
-            'TestSets, Builds',
+            ', Builds',
             'Builds.iRevision',
             ' AND Builds.idBuild      = TestSets.idBuild'
@@ -784,5 +784,5 @@
         ),
         ksResultsGroupingTypeSchedGroup: (
-            'TestSets, TestBoxes',
+            ', TestBoxes',
             'TestBoxes.idSchedGroup',
             ' AND TestSets.idGenTestBox = TestBoxes.idGenTestBox',
@@ -809,5 +809,5 @@
         self.oUserAccountLogic   = None;
 
-    def _getTimePeriodQueryPart(self, tsNow, sInterval):
+    def _getTimePeriodQueryPart(self, tsNow, sInterval, sExtraIndent = ''):
         """
         Get part of SQL query responsible for SELECT data within
@@ -819,18 +819,19 @@
         if tsNow is None:
             sRet =        '(TestSets.tsDone IS NULL OR TestSets.tsDone >= (CURRENT_TIMESTAMP - \'%s\'::interval))\n' \
-                   '   AND TestSets.tsCreated >= (CURRENT_TIMESTAMP  - \'%s\'::interval - \'%u months\'::interval)\n' \
-                 % (sInterval, sInterval, cMonthsMourningPeriod);
+                   '%s   AND TestSets.tsCreated >= (CURRENT_TIMESTAMP  - \'%s\'::interval - \'%u months\'::interval)\n' \
+                 % ( sInterval,
+                     sExtraIndent, sInterval, cMonthsMourningPeriod);
         else:
             sTsNow = '\'%s\'::TIMESTAMP' % (tsNow,); # It's actually a string already. duh.
             sRet =        'TestSets.tsCreated <= %s\n' \
-                   '   AND TestSets.tsCreated >= (%s  - \'%s\'::interval - \'%u months\'::interval)\n' \
-                   '   AND (TestSets.tsDone IS NULL OR TestSets.tsDone >= (%s - \'%s\'::interval))\n' \
+                   '%   AND TestSets.tsCreated >= (%s  - \'%s\'::interval - \'%u months\'::interval)\n' \
+                   '%   AND (TestSets.tsDone IS NULL OR TestSets.tsDone >= (%s - \'%s\'::interval))\n' \
                  % ( sTsNow,
-                     sTsNow, sInterval, cMonthsMourningPeriod,
-                     sTsNow, sInterval );
+                     sExtraIndent, sTsNow, sInterval, cMonthsMourningPeriod,
+                     sExtraIndent, sTsNow, sInterval );
         return sRet
 
-    def fetchResultsForListing(self, iStart, cMaxRows, tsNow, sInterval, enmResultSortBy,
-                               enmResultsGroupingType, iResultsGroupingValue, fOnlyFailures):
+    def fetchResultsForListing(self, iStart, cMaxRows, tsNow, sInterval, enmResultSortBy, # pylint: disable=R0913
+                               enmResultsGroupingType, iResultsGroupingValue, fOnlyFailures, fOnlyNeedingReason):
         """
         Fetches TestResults table content.
@@ -902,12 +903,13 @@
                   '       Builds,\n' \
                   '       TestBoxes,\n' \
-                  '       TestResults LEFT OUTER JOIN TestResultFailures\n' \
-                  '            ON TestResults.idTestResult    = TestResultFailures.idTestResult\n' \
-                  '           AND TestResultFailures.tsExpire = \'infinity\'::TIMESTAMP';
+                  '       TestResults\n' \
+                  '       LEFT OUTER JOIN TestResultFailures\n' \
+                  '                    ON     TestResults.idTestResult    = TestResultFailures.idTestResult\n' \
+                  '                       AND TestResultFailures.tsExpire = \'infinity\'::TIMESTAMP';
         if sSortingOrderBy is not None and sSortingOrderBy.find('FailureReason') >= 0:
             sQuery += '\n' \
                       '       LEFT OUTER JOIN FailureReasons\n' \
-                      '            ON TestResultFailures.idFailureReason = FailureReasons.idFailureReason\n' \
-                      '           AND FailureReasons.tsExpire            = \'infinity\'::TIMESTAMP';
+                      '                    ON     TestResultFailures.idFailureReason = FailureReasons.idFailureReason\n' \
+                      '                       AND FailureReasons.tsExpire            = \'infinity\'::TIMESTAMP';
         sQuery += ',\n'\
                   '       TestCases,\n' \
@@ -922,9 +924,19 @@
                   '                 TestSets.idGenTestCase AS idGenTestCase,\n' \
                   '                 TestSets.idGenTestCaseArgs AS idGenTestCaseArgs\n' \
-                  '          FROM  ' + sGroupingTables + sSortingTables + '\n' \
-                  '          WHERE ' + self._getTimePeriodQueryPart(tsNow, sInterval);
-        if fOnlyFailures:
-            sQuery += '            AND TestSets.enmStatus != \'success\'::TestStatus_T' \
-                      '            AND TestSets.enmStatus != \'running\'::TestStatus_T';
+                  '          FROM  TestSets';
+        if fOnlyNeedingReason:
+            sQuery += '\n' \
+                      '          LEFT OUTER JOIN TestResultFailures\n' \
+                      '                       ON     TestSets.idTestSet          = TestResultFailures.idTestSet\n' \
+                      '                          AND TestResultFailures.tsExpire = \'infinity\'::TIMESTAMP';
+        sQuery += sGroupingTables.replace(',', ',\n                ');
+        sQuery += sSortingTables.replace( ',', ',\n                ');
+        sQuery += '\n' \
+                  '          WHERE ' + self._getTimePeriodQueryPart(tsNow, sInterval, '         ');
+        if fOnlyFailures or fOnlyNeedingReason:
+            sQuery += '            AND TestSets.enmStatus != \'success\'::TestStatus_T\n' \
+                      '            AND TestSets.enmStatus != \'running\'::TestStatus_T\n';
+        if fOnlyNeedingReason:
+            sQuery += '            AND TestResultFailures.idTestSet IS NULL\n';
         if sGroupingField is not None:
             sQuery += '            AND %s = %d\n' % (sGroupingField, iResultsGroupingValue,);
@@ -972,5 +984,5 @@
         return aoRows
 
-    def getEntriesCount(self, tsNow, sInterval, enmResultsGroupingType, iResultsGroupingValue, fOnlyFailures):
+    def getEntriesCount(self, tsNow, sInterval, enmResultsGroupingType, iResultsGroupingValue, fOnlyFailures, fOnlyNeedingReason):
         """
         Get number of table records.
@@ -997,10 +1009,19 @@
         # Construct the query.
         #
-        sQuery = 'SELECT COUNT(idTestSet)\n' \
-                 'FROM   ' + sGroupingTables + '\n' \
-                 'WHERE  ' + self._getTimePeriodQueryPart(tsNow, sInterval);
-        if fOnlyFailures:
-            sQuery += '   AND TestSets.enmStatus != \'success\'::TestStatus_T' \
-                      '   AND TestSets.enmStatus != \'running\'::TestStatus_T';
+        sQuery = 'SELECT COUNT(TestSets.idTestSet)\n' \
+                 'FROM   TestSets';
+        if fOnlyNeedingReason:
+            sQuery += '\n' \
+                      '       LEFT OUTER JOIN TestResultFailures\n' \
+                      '                    ON     TestSets.idTestSet          = TestResultFailures.idTestSet\n' \
+                      '                       AND TestResultFailures.tsExpire = \'infinity\'::TIMESTAMP';
+        sQuery += sGroupingTables.replace(',', ',\n       ');
+        sQuery += '\n' \
+                  'WHERE  ' + self._getTimePeriodQueryPart(tsNow, sInterval);
+        if fOnlyFailures or fOnlyNeedingReason:
+            sQuery += '   AND TestSets.enmStatus != \'success\'::TestStatus_T\n' \
+                      '   AND TestSets.enmStatus != \'running\'::TestStatus_T\n';
+        if fOnlyNeedingReason:
+            sQuery += '   AND TestResultFailures.idTestSet IS NULL\n';
         if sGroupingField is not None:
             sQuery += '   AND %s = %d\n' % (sGroupingField, iResultsGroupingValue,);
@@ -1062,25 +1083,19 @@
         """
 
-        ## @todo do all in one query.
-        self._oDb.execute('SELECT DISTINCT TestBoxes.idTestBox, TestBoxes.idGenTestBox\n'
-                          'FROM TestBoxes, TestSets\n'
-                          'WHERE TestSets.idGenTestBox = TestBoxes.idGenTestBox\n'
-                          '  AND ' + self._getTimePeriodQueryPart(tsNow, sPeriod) +
-                          'ORDER BY TestBoxes.idTestBox, TestBoxes.idGenTestBox DESC' );
-        idPrevTestBox = -1;
-        asIdGenTestBoxes = [];
+        self._oDb.execute('SELECT TestBoxes.*\n'
+                          'FROM   TestBoxes,\n'
+                          '       ( SELECT TestBoxes.idTestBox  AS idTestBox,\n'
+                          '         MAX(TestBoxes.idGenTestBox) AS idGenTestBox\n'
+                          '         FROM   TestBoxes, TestSets\n'
+                          '         WHERE  TestSets.idGenTestBox = TestBoxes.idGenTestBox\n'
+                          '           AND ' + self._getTimePeriodQueryPart(tsNow, sPeriod, '        ') +
+                          '         GROUP BY TestBoxes.idTestBox\n'
+                          '         ORDER BY TestBoxes.idTestBox\n'
+                          '       ) AS TestBoxIDs\n'
+                          'WHERE  TestBoxes.idGenTestBox = TestBoxIDs.idGenTestBox\n'
+                          'ORDER BY TestBoxes.sName\n' );
+        aoRet = []
         for aoRow in self._oDb.fetchAll():
-            if aoRow[0] != idPrevTestBox:
-                idPrevTestBox = aoRow[0];
-                asIdGenTestBoxes.append(str(aoRow[1]));
-
-        aoRet = []
-        if len(asIdGenTestBoxes) > 0:
-            self._oDb.execute('SELECT *\n'
-                              'FROM TestBoxes\n'
-                              'WHERE idGenTestBox IN (' + ','.join(asIdGenTestBoxes) + ')\n'
-                              'ORDER BY sName');
-            for aoRow in self._oDb.fetchAll():
-                aoRet.append(TestBoxData().initFromDbRow(aoRow));
+            aoRet.append(TestBoxData().initFromDbRow(aoRow));
         return aoRet
 
@@ -2182,7 +2197,7 @@
     def _resolveSetTestIdIfMissing(self, oData):
         """ Resolve any missing idTestSet reference (it's a duplicate for speed efficiency). """
-        if oData.idTestSet is None and oData.idTestResult is not None :
+        if oData.idTestSet is None and oData.idTestResult is not None:
             self._oDb.execute('SELECT idTestSet FROM TestResults WHERE idTestResult = %s', (oData.idTestResult,));
-            oData.idTestResult = self._oDb.fetchOne()[0];
+            oData.idTestSet = self._oDb.fetchOne()[0];
         return oData;
 
Index: /trunk/src/VBox/ValidationKit/testmanager/webui/wuibase.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/webui/wuibase.py	(revision 61266)
+++ /trunk/src/VBox/ValidationKit/testmanager/webui/wuibase.py	(revision 61267)
@@ -919,5 +919,5 @@
 
 
-    def _actionGenericFormPost(self, sMode, fnLogicAction, oDataType, oFormType, sRedirectTo, fStrict=True):
+    def _actionGenericFormPost(self, sMode, fnLogicAction, oDataType, oFormType, sRedirectTo, fStrict = True):
         """
         Generic POST request handling from a WuiFormContentBase child.
@@ -989,4 +989,5 @@
         return self._actionGenericFormPost(WuiFormContentBase.ksMode_Add, oLogic.addEntry, oDataType, oFormType,
                                            '?' + webutils.encodeUrlParams({self.ksParamAction: sRedirAction}), fStrict=fStrict)
+
     def _actionGenericFormEditPost(self, oDataType, oLogicType, oFormType, sRedirAction, fStrict = True):
         """
Index: /trunk/src/VBox/ValidationKit/testmanager/webui/wuimain.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/webui/wuimain.py	(revision 61266)
+++ /trunk/src/VBox/ValidationKit/testmanager/webui/wuimain.py	(revision 61267)
@@ -154,5 +154,7 @@
     ksParamGroupMemberId        = 'GroupMemberId'
     ## Optional parameter for indicating whether to restrict the listing to failures only.
-    ksParamOnlyFailures         = 'OnlyFailures'
+    ksParamOnlyFailures         = 'OnlyFailures';
+    ## The sheriff parameter for getting failures needing a reason or two assigned to them.
+    ksParamOnlyNeedingReason    = 'OnlyNeedingReason';
     ## Result listing sorting.
     ksParamTestResultsSortBy    = 'enmSortBy'
@@ -271,12 +273,20 @@
 
         # Shorthand to keep within margins.
-        sActUrlBase = self._sActionUrlBase;
-        sOFail = '&%s' % webutils.encodeUrlParams({self.ksParamOnlyFailures: True});
+        sActUrlBase   = self._sActionUrlBase;
+        sOnlyFailures = '&%s%s' % ( webutils.encodeUrlParams({self.ksParamOnlyFailures: True}), sExtraTimeNav, );
+        sSheriff      = '&%s%s' % ( webutils.encodeUrlParams({self.ksParamOnlyNeedingReason: True}), sExtraTimeNav, );
 
         self._aaoMenus = \
         [
             [
-                'Inbox',            sActUrlBase + 'TODO', ## @todo list of failures that needs categorizing.
-                []
+                'Sheriff',     sActUrlBase + self.ksActionResultsUnGrouped + sSheriff,
+                [
+                    [ 'Ungrouped results',           sActUrlBase + self.ksActionResultsUnGrouped           + sSheriff ],
+                    [ 'Grouped by Scheduling Group', sActUrlBase + self.ksActionResultsGroupedBySchedGroup + sSheriff ],
+                    [ 'Grouped by Test Group',       sActUrlBase + self.ksActionResultsGroupedByTestGroup  + sSheriff ],
+                    [ 'Grouped by TestBox',          sActUrlBase + self.ksActionResultsGroupedByTestBox    + sSheriff ],
+                    [ 'Grouped by Test Case',        sActUrlBase + self.ksActionResultsGroupedByTestCase   + sSheriff ],
+                    [ 'Grouped by Revision',         sActUrlBase + self.ksActionResultsGroupedByBuildRev   + sSheriff ],
+                ]
             ],
             [
@@ -300,12 +310,12 @@
             ],
             [
-                'Test Failures',     sActUrlBase + self.ksActionResultsUnGrouped + sOFail + sExtraTimeNav,
+                'Test Failures',     sActUrlBase + self.ksActionResultsUnGrouped + sOnlyFailures,
                 [
-                    [ 'Ungrouped results',           sActUrlBase + self.ksActionResultsUnGrouped          +sOFail+sExtraTimeNav],
-                    [ 'Grouped by Scheduling Group', sActUrlBase + self.ksActionResultsGroupedBySchedGroup+sOFail+sExtraTimeNav],
-                    [ 'Grouped by Test Group',       sActUrlBase + self.ksActionResultsGroupedByTestGroup +sOFail+sExtraTimeNav],
-                    [ 'Grouped by TestBox',          sActUrlBase + self.ksActionResultsGroupedByTestBox   +sOFail+sExtraTimeNav],
-                    [ 'Grouped by Test Case',        sActUrlBase + self.ksActionResultsGroupedByTestCase  +sOFail+sExtraTimeNav],
-                    [ 'Grouped by Revision',         sActUrlBase + self.ksActionResultsGroupedByBuildRev  +sOFail+sExtraTimeNav],
+                    [ 'Ungrouped results',           sActUrlBase + self.ksActionResultsUnGrouped           + sOnlyFailures ],
+                    [ 'Grouped by Scheduling Group', sActUrlBase + self.ksActionResultsGroupedBySchedGroup + sOnlyFailures ],
+                    [ 'Grouped by Test Group',       sActUrlBase + self.ksActionResultsGroupedByTestGroup  + sOnlyFailures ],
+                    [ 'Grouped by TestBox',          sActUrlBase + self.ksActionResultsGroupedByTestBox    + sOnlyFailures ],
+                    [ 'Grouped by Test Case',        sActUrlBase + self.ksActionResultsGroupedByTestCase   + sOnlyFailures ],
+                    [ 'Grouped by Revision',         sActUrlBase + self.ksActionResultsGroupedByBuildRev   + sOnlyFailures ],
                 ]
             ],
@@ -326,6 +336,10 @@
     def _isMenuMatch(self, sMenuUrl, sActionParam):
         if super(WuiMain, self)._isMenuMatch(sMenuUrl, sActionParam):
+            fOnlyNeedingReason = self.getBoolParam(self.ksParamOnlyNeedingReason, fDefault = False);
+            if fOnlyNeedingReason:
+                return (sMenuUrl.find(self.ksParamOnlyNeedingReason) > 0);
             fOnlyFailures = self.getBoolParam(self.ksParamOnlyFailures, fDefault = False);
-            return (sMenuUrl.find(self.ksParamOnlyFailures) > 0) == fOnlyFailures;
+            return (sMenuUrl.find(self.ksParamOnlyFailures) > 0) == fOnlyFailures \
+                and sMenuUrl.find(self.ksParamOnlyNeedingReason) < 0;
         return False;
 
@@ -705,17 +719,18 @@
         oListContentType is a child of WuiListContentBase.
         """
-        cItemsPerPage     = self.getIntParam(self.ksParamItemsPerPage,  iMin =  2, iMax =   9999, iDefault = 128);
-        iPage             = self.getIntParam(self.ksParamPageNo,        iMin =  0, iMax = 999999, iDefault = 0);
-        tsEffective       = self.getEffectiveDateParam();
-        iGroupMemberId    = self.getIntParam(self.ksParamGroupMemberId, iMin = -1, iMax = 999999, iDefault = -1);
-        fOnlyFailures     = self.getBoolParam(self.ksParamOnlyFailures, fDefault = False);
-        enmResultSortBy   = self.getStringParam(self.ksParamTestResultsSortBy,
-                                                asValidValues = TestResultLogic.kasResultsSortBy,
-                                                sDefault = TestResultLogic.ksResultsSortByRunningAndStart);
+        cItemsPerPage       = self.getIntParam(self.ksParamItemsPerPage,  iMin =  2, iMax =   9999, iDefault = 128);
+        iPage               = self.getIntParam(self.ksParamPageNo,        iMin =  0, iMax = 999999, iDefault = 0);
+        tsEffective         = self.getEffectiveDateParam();
+        iGroupMemberId      = self.getIntParam(self.ksParamGroupMemberId, iMin = -1, iMax = 999999, iDefault = -1);
+        fOnlyFailures       = self.getBoolParam(self.ksParamOnlyFailures, fDefault = False);
+        fOnlyNeedingReason  = self.getBoolParam(self.ksParamOnlyNeedingReason, fDefault = False);
+        enmResultSortBy     = self.getStringParam(self.ksParamTestResultsSortBy,
+                                                  asValidValues = TestResultLogic.kasResultsSortBy,
+                                                  sDefault = TestResultLogic.ksResultsSortByRunningAndStart);
 
         # Get testing results period and validate it
-        asValidValues     = [x for (x, _, _) in self.kaoResultPeriods]
-        sCurPeriod        = self.getStringParam(self.ksParamEffectivePeriod, asValidValues = asValidValues,
-                                                sDefault = self.ksResultPeriodDefault)
+        asValidValues       = [x for (x, _, _) in self.kaoResultPeriods]
+        sCurPeriod          = self.getStringParam(self.ksParamEffectivePeriod, asValidValues = asValidValues,
+                                                  sDefault = self.ksResultPeriodDefault)
         assert sCurPeriod != ''; # Impossible!
 
@@ -790,5 +805,6 @@
                                                     enmResultsGroupingType = enmResultsGroupingType,
                                                     iResultsGroupingValue = idMember,
-                                                    fOnlyFailures = fOnlyFailures);
+                                                    fOnlyFailures = fOnlyFailures,
+                                                    fOnlyNeedingReason = fOnlyNeedingReason);
             if cEntries == 0: # Do not display empty groups
                 continue
@@ -800,5 +816,6 @@
                                                             enmResultsGroupingType = enmResultsGroupingType,
                                                             iResultsGroupingValue = idMember,
-                                                            fOnlyFailures = fOnlyFailures);
+                                                            fOnlyFailures = fOnlyFailures,
+                                                            fOnlyNeedingReason = fOnlyNeedingReason);
             cEntriesMax = max(cEntriesMax, cEntries)
 
Index: /trunk/src/VBox/ValidationKit/testmanager/webui/wuitestresult.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/webui/wuitestresult.py	(revision 61266)
+++ /trunk/src/VBox/ValidationKit/testmanager/webui/wuitestresult.py	(revision 61267)
@@ -394,5 +394,4 @@
                 oForm.addTextHidden(TestResultFailureData.ksParam_uidAuthor, oData.uidAuthor);
                 oForm.addSubmit('Change Reason');
-
             else:
                 oForm.addComboBox(TestResultFailureData.ksParam_idFailureReason, -1, 'Reason', aoFailureReasons,
@@ -808,4 +807,5 @@
         # Reason:
         oReason = None;
+        #assert (oEntry.oFailureReason is None) == (oEntry.tsFailureReasonAssigned is None);
         if oEntry.oFailureReason is not None:
             sReasonTitle  = 'Reason:  \t%s\n' % ( oEntry.oFailureReason.sShort, );
