Index: /trunk/src/VBox/ValidationKit/testmanager/core/failurecategory.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/core/failurecategory.py	(revision 61249)
+++ /trunk/src/VBox/ValidationKit/testmanager/core/failurecategory.py	(revision 61250)
@@ -31,5 +31,7 @@
 
 # Validation Kit imports.
-from testmanager.core.base          import ModelDataBase, ModelLogicBase, TMRowInUse, TMInvalidData, TMRowNotFound;
+from testmanager.core.base          import ModelDataBase, ModelLogicBase, TMRowInUse, TMInvalidData, TMRowNotFound, \
+                                           ChangeLogEntry, AttributeChangeEntry;
+from testmanager.core.useraccount   import UserAccountLogic;
 
 
@@ -137,4 +139,49 @@
             aoRows.append(FailureCategoryData().initFromDbRow(aoRow))
         return aoRows
+
+
+    def fetchForChangeLog(self, idFailureCategory, iStart, cMaxRows, tsNow): # pylint: disable=R0914
+        """
+        Fetches change log entries for a failure reason.
+
+        Returns an array of ChangeLogEntry instance and an indicator whether
+        there are more entries.
+        Raises exception on error.
+        """
+        if tsNow is None:
+            tsNow = self._oDb.getCurrentTimestamp();
+
+        # 1. Get a list of the relevant changes.
+        self._oDb.execute('SELECT * FROM FailureCategories WHERE idFailureCategory = %s AND tsEffective <= %s\n'
+                          'ORDER BY tsEffective DESC\n'
+                          'LIMIT %s OFFSET %s\n'
+                          , ( idFailureCategory, tsNow, cMaxRows + 1, iStart, ));
+        aoRows = [];
+        for aoChange in self._oDb.fetchAll():
+            aoRows.append(FailureCategoryData().initFromDbRow(aoChange));
+
+        # 2. Calculate the changes.
+        aoEntries = [];
+        for i in xrange(0, len(aoRows) - 1):
+            oNew = aoRows[i];
+            oOld = aoRows[i + 1];
+
+            aoChanges = [];
+            for sAttr in oNew.getDataAttributes():
+                if sAttr not in [ 'tsEffective', 'tsExpire', 'uidAuthor', ]:
+                    oOldAttr = getattr(oOld, sAttr);
+                    oNewAttr = getattr(oNew, sAttr);
+                    if oOldAttr != oNewAttr:
+                        aoChanges.append(AttributeChangeEntry(sAttr, oNewAttr, oOldAttr, str(oNewAttr), str(oOldAttr)));
+
+            aoEntries.append(ChangeLogEntry(oNew.uidAuthor, None, oNew.tsEffective, oNew.tsExpire, oNew, oOld, aoChanges));
+
+        # If we're at the end of the log, add the initial entry.
+        if len(aoRows) <= cMaxRows and len(aoRows) > 0:
+            oNew = aoRows[-1];
+            aoEntries.append(ChangeLogEntry(oNew.uidAuthor, None, oNew.tsEffective, oNew.tsExpire, oNew, None, []));
+
+        return (UserAccountLogic(self._oDb).resolveChangeLogAuthors(aoEntries), len(aoRows) > cMaxRows);
+
 
     def getFailureCategoriesForCombo(self, tsEffective = None):
Index: /trunk/src/VBox/ValidationKit/testmanager/core/failurereason.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/core/failurereason.py	(revision 61249)
+++ /trunk/src/VBox/ValidationKit/testmanager/core/failurereason.py	(revision 61250)
@@ -31,5 +31,6 @@
 
 # Validation Kit imports.
-from testmanager.core.base              import ModelDataBase, ModelLogicBase, TMRowNotFound, TMInvalidData, TMRowInUse;
+from testmanager.core.base              import ModelDataBase, ModelLogicBase, TMRowNotFound, TMInvalidData, TMRowInUse, \
+                                               AttributeChangeEntry, ChangeLogEntry;
 from testmanager.core.useraccount       import UserAccountLogic;
 
@@ -151,27 +152,69 @@
         Fetches Failure Category records.
 
-        Returns an array (list) of FailureReasonData items, empty list if none.
+        Returns an array (list) of FailureReasonDataEx items, empty list if none.
         Raises exception on error.
         """
+        self._ensureCachesPresent();
+
+        if tsNow is None:
+            self._oDb.execute('SELECT   FailureReasons.*,\n'
+                              '         FailureCategories.sShort AS sCategory\n'
+                              'FROM     FailureReasons,\n'
+                              '         FailureCategories\n'
+                              'WHERE    FailureReasons.tsExpire             = \'infinity\'::TIMESTAMP\n'
+                              '     AND FailureCategories.idFailureCategory = FailureReasons.idFailureCategory\n'
+                              '     AND FailureCategories.tsExpire          = \'infinity\'::TIMESTAMP\n'
+                              'ORDER BY sCategory ASC, sShort ASC\n'
+                              'LIMIT %s OFFSET %s\n'
+                              , (cMaxRows, iStart,));
+        else:
+            self._oDb.execute('SELECT   FailureReasons.*,\n'
+                              '         FailureCategories.sShort AS sCategory\n'
+                              'FROM     FailureReasons,\n'
+                              '         FailureCategories\n'
+                              'WHERE    FailureReasons.tsExpire     > %s\n'
+                              '     AND FailureReasons.tsEffective <= %s\n'
+                              '     AND FailureCategories.idFailureCategory = FailureReasons.idFailureCategory\n'
+                              '     AND FailureReasons.tsExpire     > %s\n'
+                              '     AND FailureReasons.tsEffective <= %s\n'
+                              'ORDER BY sCategory ASC, sShort ASC\n'
+                              'LIMIT %s OFFSET %s\n'
+                              , (tsNow, tsNow, tsNow, tsNow, cMaxRows, iStart,));
+
+        aoRows = []
+        for aoRow in self._oDb.fetchAll():
+            aoRows.append(FailureReasonDataEx().initFromDbRowEx(aoRow, self.oCategoryLogic, self.oUserAccountLogic));
+        return aoRows
+
+    def fetchForListingInCategory(self, iStart, cMaxRows, tsNow, idFailureCategory):
+        """
+        Fetches Failure Category records.
+
+        Returns an array (list) of FailureReasonDataEx items, empty list if none.
+        Raises exception on error.
+        """
+        self._ensureCachesPresent();
 
         if tsNow is None:
             self._oDb.execute('SELECT   *\n'
                               'FROM     FailureReasons\n'
-                              'WHERE    tsExpire = \'infinity\'::TIMESTAMP\n'
-                              'ORDER BY idFailureReason DESC\n'
+                              'WHERE    tsExpire          = \'infinity\'::TIMESTAMP\n'
+                              '     AND idFailureCategory = %s\n'
+                              'ORDER BY sShort ASC\n'
                               'LIMIT %s OFFSET %s\n'
-                              , (cMaxRows, iStart,));
+                              , ( idFailureCategory, cMaxRows, iStart,));
         else:
             self._oDb.execute('SELECT   *\n'
                               'FROM     FailureReasons\n'
-                              'WHERE    tsExpire     > %s\n'
-                              '     AND tsEffective <= %s\n'
-                              'ORDER BY idFailureReason DESC\n'
+                              'WHERE    idFailureCategory = %s\n'
+                              '     AND tsExpire          > %s\n'
+                              '     AND tsEffective      <= %s\n'
+                              'ORDER BY sShort ASC\n'
                               'LIMIT %s OFFSET %s\n'
-                              , (tsNow, tsNow, cMaxRows, iStart,));
+                              , ( tsNow, tsNow, idFailureCategory, cMaxRows, iStart,));
 
         aoRows = []
         for aoRow in self._oDb.fetchAll():
-            aoRows.append(FailureReasonData().initFromDbRow(aoRow))
+            aoRows.append(FailureReasonDataEx().initFromDbRowEx(aoRow, self.oCategoryLogic, self.oUserAccountLogic));
         return aoRows
 
@@ -204,4 +247,58 @@
         return [(-1, sFirstEntry, '')] + aoRows;
 
+
+    def fetchForChangeLog(self, idFailureReason, iStart, cMaxRows, tsNow): # pylint: disable=R0914
+        """
+        Fetches change log entries for a failure reason.
+
+        Returns an array of ChangeLogEntry instance and an indicator whether
+        there are more entries.
+        Raises exception on error.
+        """
+        self._ensureCachesPresent();
+
+        if tsNow is None:
+            tsNow = self._oDb.getCurrentTimestamp();
+
+        # 1. Get a list of the relevant changes.
+        self._oDb.execute('SELECT * FROM FailureReasons WHERE idFailureReason = %s AND tsEffective <= %s\n'
+                          'ORDER BY tsEffective DESC\n'
+                          'LIMIT %s OFFSET %s\n'
+                          , ( idFailureReason, tsNow, cMaxRows + 1, iStart, ));
+        aoRows = [];
+        for aoChange in self._oDb.fetchAll():
+            aoRows.append(FailureReasonData().initFromDbRow(aoChange));
+
+        # 2. Calculate the changes.
+        aoEntries = [];
+        for i in xrange(0, len(aoRows) - 1):
+            oNew = aoRows[i];
+            oOld = aoRows[i + 1];
+
+            aoChanges = [];
+            for sAttr in oNew.getDataAttributes():
+                if sAttr not in [ 'tsEffective', 'tsExpire', 'uidAuthor', ]:
+                    oOldAttr = getattr(oOld, sAttr);
+                    oNewAttr = getattr(oNew, sAttr);
+                    if oOldAttr != oNewAttr:
+                        if sAttr == 'idFailureCategory':
+                            oCat = self.oCategoryLogic.cachedLookup(oOldAttr);
+                            if oCat is not None:
+                                oOldAttr = '%s (%s)' % (oOldAttr, oCat.sShort, );
+                            oCat = self.oCategoryLogic.cachedLookup(oNewAttr);
+                            if oCat is not None:
+                                oNewAttr = '%s (%s)' % (oNewAttr, oCat.sShort, );
+                        aoChanges.append(AttributeChangeEntry(sAttr, oNewAttr, oOldAttr, str(oNewAttr), str(oOldAttr)));
+
+            aoEntries.append(ChangeLogEntry(oNew.uidAuthor, None, oNew.tsEffective, oNew.tsExpire, oNew, oOld, aoChanges));
+
+        # If we're at the end of the log, add the initial entry.
+        if len(aoRows) <= cMaxRows and len(aoRows) > 0:
+            oNew = aoRows[-1];
+            aoEntries.append(ChangeLogEntry(oNew.uidAuthor, None, oNew.tsEffective, oNew.tsExpire, oNew, None, []));
+
+        return (UserAccountLogic(self._oDb).resolveChangeLogAuthors(aoEntries), len(aoRows) > cMaxRows);
+
+
     def getById(self, idFailureReason):
         """Get Failure Reason data by idFailureReason"""
@@ -333,9 +430,5 @@
 
             if self._oDb.getRowCount() == 1:
-                if self.oCategoryLogic is None:
-                    from testmanager.core.failurecategory import FailureCategoryLogic;
-                    self.oCategoryLogic = FailureCategoryLogic(self._oDb);
-                if self.oUserAccountLogic is None:
-                    self.oUserAccountLogic = UserAccountLogic(self._oDb);
+                self._ensureCachesPresent();
                 oEntry = FailureReasonDataEx().initFromDbRowEx(self._oDb.fetchOne(), self.oCategoryLogic,
                                                                self.oUserAccountLogic);
@@ -386,2 +479,13 @@
         return True;
 
+
+    def _ensureCachesPresent(self):
+        """ Ensures we've got the cache references resolved. """
+        if self.oCategoryLogic is None:
+            from testmanager.core.failurecategory import FailureCategoryLogic;
+            self.oCategoryLogic = FailureCategoryLogic(self._oDb);
+        if self.oUserAccountLogic is None:
+            self.oUserAccountLogic = UserAccountLogic(self._oDb);
+        return True;
+
+
Index: /trunk/src/VBox/ValidationKit/testmanager/core/testresults.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/core/testresults.py	(revision 61249)
+++ /trunk/src/VBox/ValidationKit/testmanager/core/testresults.py	(revision 61250)
@@ -38,8 +38,9 @@
 from testmanager                    import config;
 from testmanager.core.base          import ModelDataBase, ModelLogicBase, ModelDataBaseTestCase, TMExceptionBase, \
-                                           TMTooManyRows, TMInvalidData, TMRowNotFound, TMRowAlreadyExists;
+                                           TMTooManyRows, TMInvalidData, TMRowNotFound, TMRowAlreadyExists, \
+                                           ChangeLogEntry, AttributeChangeEntry;
 from testmanager.core.testgroup     import TestGroupData;
 from testmanager.core.build         import BuildDataEx;
-from testmanager.core.failurereason import FailureReasonLogic;
+from testmanager.core.failurereason import FailureReasonLogic, FailureReasonData;
 from testmanager.core.testbox       import TestBoxData;
 from testmanager.core.testcase      import TestCaseData;
@@ -126,4 +127,19 @@
         return self;
 
+    def initFromDbWithId(self, oDb, idTestResult, tsNow = None, sPeriodBack = None):
+        """
+        Initialize from the database, given the ID of a row.
+        """
+        _ = tsNow;
+        _ = sPeriodBack;
+        oDb.execute('SELECT *\n'
+                    'FROM   TestResults\n'
+                    'WHERE  idTestResult = %s\n'
+                    , ( idTestResult,));
+        aoRow = oDb.fetchOne()
+        if aoRow is None:
+            raise TMRowNotFound('idTestResult=%s not found' % (idTestResult,));
+        return self.initFromDbRow(aoRow);
+
     def isFailure(self):
         """ Check if it's a real failure. """
@@ -430,4 +446,6 @@
     kasAllowNullAttributes      = ['tsEffective', 'tsExpire', 'uidAuthor', 'sComment' ];
 
+    kcDbColumns                 = 6;
+
     def __init__(self):
         ModelDataBase.__init__(self)
@@ -467,4 +485,5 @@
         if aoRow is None:
             raise TMRowNotFound('idTestResult=%s not found (tsNow=%s, sPeriodBack=%s)' % (idTestResult, tsNow, sPeriodBack));
+        assert len(aoRow) == self.kcDbColumns;
         return self.initFromDbRow(aoRow);
 
@@ -1922,4 +1941,107 @@
         ModelLogicBase.__init__(self, oDb)
 
+    def fetchForChangeLog(self, idTestResult, iStart, cMaxRows, tsNow): # pylint: disable=R0914
+        """
+        Fetches change log entries for a failure reason.
+
+        Returns an array of ChangeLogEntry instance and an indicator whether
+        there are more entries.
+        Raises exception on error.
+        """
+
+        if tsNow is None:
+            tsNow = self._oDb.getCurrentTimestamp();
+
+        # 1. Get a list of the changes from both TestResultFailures and assoicated
+        #    FailureReasons.  The latter is useful since the failure reason
+        #    description may evolve along side the invidiual failure analysis.
+        self._oDb.execute('( SELECT trf.tsEffective AS tsEffectiveChangeLog,\n'
+                          '         trf.uidAuthor   AS uidAuthorChangeLog,\n'
+                          '         trf.*,\n'
+                          '         fr.*\n'
+                          '  FROM   TestResultFailures trf,\n'
+                          '         FailureReasons fr\n'
+                          '  WHERE  trf.idTestResult = %s\n'
+                          '     AND trf.tsEffective <= %s\n'
+                          '     AND trf.idFailureReason = fr.idFailureReason\n'
+                          '     AND fr.tsEffective      <= trf.tsEffective\n'
+                          '     AND fr.tsExpire         >  trf.tsEffective\n'
+                          ')\n'
+                          'UNION\n'
+                          '( SELECT fr.tsEffective AS tsEffectiveChangeLog,\n'
+                          '         fr.uidAuthor   AS uidAuthorChangeLog,\n'
+                          '         trf.*,\n'
+                          '         fr.*\n'
+                          '  FROM   TestResultFailures trf,\n'
+                          '         FailureReasons fr\n'
+                          '  WHERE  trf.idTestResult    = %s\n'
+                          '     AND trf.tsEffective    <= %s\n'
+                          '     AND trf.idFailureReason = fr.idFailureReason\n'
+                          '     AND fr.tsEffective      > trf.tsEffective\n'
+                          '     AND fr.tsEffective      < trf.tsExpire\n'
+                          ')\n'
+                          'ORDER BY tsEffectiveChangeLog DESC\n'
+                          'LIMIT %s OFFSET %s\n'
+                          , ( idTestResult, tsNow, idTestResult, tsNow, cMaxRows + 1, iStart, ));
+
+        aaoRows = [];
+        for aoChange in self._oDb.fetchAll():
+            oTrf = TestResultFailureDataEx().initFromDbRow(aoChange[2:]);
+            oFr  = FailureReasonData().initFromDbRow(aoChange[(2+TestResultFailureData.kcDbColumns):]);
+            oTrf.oFailureReason = oFr;
+            aaoRows.append([aoChange[0], aoChange[1], oTrf, oFr]);
+
+        # 2. Calculate the changes.
+        oFailureCategoryLogic = None;
+        aoEntries = [];
+        for i in xrange(0, len(aaoRows) - 1):
+            aoNew = aaoRows[i];
+            aoOld = aaoRows[i + 1];
+
+            aoChanges = [];
+            oNew = aoNew[2];
+            oOld = aoOld[2];
+            for sAttr in oNew.getDataAttributes():
+                if sAttr not in [ 'tsEffective', 'tsExpire', 'uidAuthor', 'oFailureReason', 'oAuthor' ]:
+                    oOldAttr = getattr(oOld, sAttr);
+                    oNewAttr = getattr(oNew, sAttr);
+                    if oOldAttr != oNewAttr:
+                        if sAttr == 'idFailureReason':
+                            oNewAttr = '%s (%s)' % (oNewAttr, oNew.oFailureReason.sShort, );
+                            oOldAttr = '%s (%s)' % (oOldAttr, oOld.oFailureReason.sShort, );
+                        aoChanges.append(AttributeChangeEntry(sAttr, oNewAttr, oOldAttr, str(oNewAttr), str(oOldAttr)));
+            if oOld.idFailureReason == oNew.idFailureReason:
+                oNew = aoNew[3];
+                oOld = aoOld[3];
+                for sAttr in oNew.getDataAttributes():
+                    if sAttr not in [ 'tsEffective', 'tsExpire', 'uidAuthor', ]:
+                        oOldAttr = getattr(oOld, sAttr);
+                        oNewAttr = getattr(oNew, sAttr);
+                        if oOldAttr != oNewAttr:
+                            if sAttr == 'idFailureCategory':
+                                if oFailureCategoryLogic is None:
+                                    from testmanager.core.failurecategory import FailureCategoryLogic;
+                                    oFailureCategoryLogic = FailureCategoryLogic(self._oDb);
+                                oCat = oFailureCategoryLogic.cachedLookup(oNewAttr);
+                                if oCat is not None:
+                                    oNewAttr = '%s (%s)' % (oNewAttr, oCat.sShort, );
+                                oCat = oFailureCategoryLogic.cachedLookup(oOldAttr);
+                                if oCat is not None:
+                                    oOldAttr = '%s (%s)' % (oOldAttr, oCat.sShort, );
+                            aoChanges.append(AttributeChangeEntry(sAttr, oNewAttr, oOldAttr, str(oNewAttr), str(oOldAttr)));
+
+
+            tsExpire    = aaoRows[i - 1][0] if i > 0 else aoNew[2].tsExpire;
+            aoEntries.append(ChangeLogEntry(aoNew[1], None, aoNew[0], tsExpire, aoNew[2], aoOld[2], aoChanges));
+
+        # If we're at the end of the log, add the initial entry.
+        if len(aaoRows) <= cMaxRows and len(aaoRows) > 0:
+            aoNew    = aaoRows[-1];
+            tsExpire = aaoRows[-1 - 1][0] if len(aaoRows) > 1 else aoNew[2].tsExpire;
+            aoEntries.append(ChangeLogEntry(aoNew[1], None, aoNew[0], tsExpire, aoNew[2], None, []));
+
+        return (UserAccountLogic(self._oDb).resolveChangeLogAuthors(aoEntries), len(aaoRows) > cMaxRows);
+
+
     def getById(self, idTestResult):
         """Get Test result failure reason data by idTestResult"""
Index: /trunk/src/VBox/ValidationKit/testmanager/webui/wuiadminfailurecategory.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/webui/wuiadminfailurecategory.py	(revision 61249)
+++ /trunk/src/VBox/ValidationKit/testmanager/webui/wuiadminfailurecategory.py	(revision 61250)
@@ -31,6 +31,23 @@
 
 # Validation Kit imports.
-from testmanager.webui.wuicontentbase  import WuiFormContentBase, WuiListContentBase, WuiTmLink
-from testmanager.core.failurecategory  import FailureCategoryData
+from testmanager.webui.wuicontentbase           import WuiFormContentBase, WuiContentBase, WuiListContentBase, WuiTmLink;
+from testmanager.webui.wuiadminfailurereason    import WuiAdminFailureReasonList;
+from testmanager.core.failurecategory           import FailureCategoryData;
+from testmanager.core.failurereason             import FailureReasonLogic;
+
+
+class WuiFailureReasonCategoryLink(WuiTmLink):
+    """ Link to a failure category. """
+    def __init__(self, idFailureCategory, sName = WuiContentBase.ksShortDetailsLink, sTitle = None, fBracketed = None):
+        if fBracketed is None:
+            fBracketed = len(sName) > 2;
+        from testmanager.webui.wuiadmin import WuiAdmin;
+        WuiTmLink.__init__(self, sName = sName,
+                           sUrlBase = WuiAdmin.ksScriptName,
+                           dParams = { WuiAdmin.ksParamAction: WuiAdmin.ksActionFailureCategoryDetails,
+                                       FailureCategoryData.ksParam_idFailureCategory: idFailureCategory, },
+                           fBracketed = fBracketed);
+        self.idFailureCategory = idFailureCategory;
+
 
 
@@ -71,4 +88,19 @@
         return True;
 
+    def _generatePostFormContent(self, oData):
+        """
+        Adds a table with the category members below the form.
+        """
+        if oData.idFailureCategory is not None and oData.idFailureCategory >= 0:
+            oLogic    = FailureReasonLogic(self._oDisp.getDb());
+            tsNow     = self._oDisp.getNow();
+            cMax      = 4096;
+            aoEntries = oLogic.fetchForListingInCategory(0, cMax, tsNow, oData.idFailureCategory)
+            if len(aoEntries) > 0:
+                oList = WuiAdminFailureReasonList(aoEntries, 0, cMax, tsNow, fnDPrint = None, oDisp = self._oDisp);
+                return [ [ 'Members', oList.show(fShowNavigation = False)[1]], ];
+        return [];
+
+
 
 class WuiFailureCategoryList(WuiListContentBase):
Index: /trunk/src/VBox/ValidationKit/testmanager/webui/wuiadminfailurereason.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/webui/wuiadminfailurereason.py	(revision 61249)
+++ /trunk/src/VBox/ValidationKit/testmanager/webui/wuiadminfailurereason.py	(revision 61250)
@@ -32,8 +32,37 @@
 # Validation Kit imports.
 from testmanager.webui.wuibase        import WuiException
-from testmanager.webui.wuicontentbase import WuiFormContentBase, WuiListContentBase, WuiTmLink
-from testmanager.core.failurereason   import FailureReasonData
-from testmanager.core.failurecategory import FailureCategoryLogic
-from testmanager.core.db              import TMDatabaseConnection
+from testmanager.webui.wuicontentbase import WuiFormContentBase, WuiListContentBase, WuiContentBase, WuiTmLink;
+from testmanager.core.failurereason   import FailureReasonData;
+from testmanager.core.failurecategory import FailureCategoryLogic;
+from testmanager.core.db              import TMDatabaseConnection;
+
+
+
+class WuiFailureReasonDetailsLink(WuiTmLink):
+    """ Short link to a failure reason. """
+    def __init__(self, idFailureReason, sName = WuiContentBase.ksShortDetailsLink, sTitle = None, fBracketed = None):
+        if fBracketed is None:
+            fBracketed = len(sName) > 2;
+        from testmanager.webui.wuiadmin import WuiAdmin;
+        WuiTmLink.__init__(self, sName = sName,
+                           sUrlBase = WuiAdmin.ksScriptName,
+                           dParams = { WuiAdmin.ksParamAction: WuiAdmin.ksActionFailureReasonDetails,
+                                       FailureReasonData.ksParam_idFailureReason: idFailureReason, },
+                           fBracketed = fBracketed);
+        self.idFailureReason = idFailureReason;
+
+
+
+class WuiFailureReasonAddLink(WuiTmLink):
+    """ Link for adding a failure reason. """
+    def __init__(self, sName = WuiContentBase.ksShortAddLink, sTitle = None, fBracketed = None):
+        if fBracketed is None:
+            fBracketed = len(sName) > 2;
+        from testmanager.webui.wuiadmin import WuiAdmin;
+        WuiTmLink.__init__(self, sName = sName,
+                           sUrlBase = WuiAdmin.ksScriptName,
+                           dParams = { WuiAdmin.ksParamAction: WuiAdmin.ksActionFailureReasonAdd, },
+                           fBracketed = fBracketed);
+
 
 
@@ -107,9 +136,10 @@
 
     def _formatListEntry(self, iEntry):
-        from testmanager.webui.wuiadmin import WuiAdmin
+        from testmanager.webui.wuiadmin                 import WuiAdmin
+        from testmanager.webui.wuiadminfailurecategory  import WuiFailureReasonCategoryLink;
         oEntry = self._aoEntries[iEntry]
 
         return [ oEntry.idFailureReason,
-                 oEntry.idFailureCategory,
+                 WuiFailureReasonCategoryLink(oEntry.idFailureCategory, sName = oEntry.oCategory.sShort, fBracketed = False),
                  oEntry.sShort,
                  oEntry.sFull,
Index: /trunk/src/VBox/ValidationKit/testmanager/webui/wuibase.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/webui/wuibase.py	(revision 61249)
+++ /trunk/src/VBox/ValidationKit/testmanager/webui/wuibase.py	(revision 61250)
@@ -104,4 +104,5 @@
         self._oSrvGlue          = oSrvGlue;
         self._oDb               = TMDatabaseConnection(self.dprint if config.g_kfWebUiSqlDebug else None, oSrvGlue = oSrvGlue);
+        self._tsNow             = None;  # Set by getEffectiveDateParam.
         self._asCheckedParams   = [];
         self._dParams           = None;  # Set by dispatchRequest.
@@ -303,4 +304,10 @@
         return self._oDb;
 
+    def getNow(self):
+        """
+        Returns the effective date.
+        """
+        return self._tsNow;
+
 
     #
@@ -506,9 +513,13 @@
         return aoListOfTestCases
 
-    def getEffectiveDateParam(self, sParamName=None):
+    def getEffectiveDateParam(self, sParamName = None):
         """
         Gets the effective date parameter.
+
         Returns a timestamp suitable for database and url parameters.
         Returns None if not found or empty.
+
+        The first call with sParamName set to None will set the internal _tsNow
+        value upon successfull return.
         """
 
@@ -535,4 +546,6 @@
             if sError is not None:
                 raise WuiException('%s parameter "%s" ("%s") is invalid: %s' % (self._sAction, sName, sValue, sError));
+            if sParamName is None and self._tsNow is None:
+                self._tsNow = sValue;
             return sValue;
 
@@ -555,5 +568,8 @@
         oDate = self._oDb.fetchOne()[0];
 
-        return str(oDate);
+        sValue = str(oDate);
+        if sParamName is None and self._tsNow is None:
+            self._tsNow = sValue;
+        return sValue;
 
     def getRedirectToParameter(self, sDefault = None):
Index: /trunk/src/VBox/ValidationKit/testmanager/webui/wuicontentbase.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/webui/wuicontentbase.py	(revision 61249)
+++ /trunk/src/VBox/ValidationKit/testmanager/webui/wuicontentbase.py	(revision 61250)
@@ -345,4 +345,13 @@
         _ = oForm; _ = oData;
         raise Exception('Reimplement me!');
+
+    def _generatePostFormContent(self, oData):
+        """
+        Generate optional content that comes below the form.
+        Returns a list of tuples, where the first tuple element is the title
+        and the second the content.  I.e. similar to show() output.
+        """
+        _ = oData;
+        return [];
 
     def _calcChangeLogEntryLinks(self, aoEntries, iEntry):
@@ -522,4 +531,44 @@
         return sContent;
 
+    def _generateTopRowFormActions(self, oData):
+        """
+        Returns a list of WuiTmLinks.
+        """
+        aoActions = [];
+        if self._sMode == self.ksMode_Show and self._fEditable:
+            # Remove _idGen and effective date since we're always editing the current data,
+            # and make sure the primary ID is present.
+            dParams = self._oDisp.getParameters();
+            if hasattr(oData, 'ksIdGenAttr'):
+                sIdGenParam = getattr(oData, 'ksParam_' + oData.ksIdGenAttr);
+                if sIdGenParam in dParams:
+                    del dParams[sIdGenParam];
+            if WuiDispatcherBase.ksParamEffectiveDate in dParams:
+                del dParams[WuiDispatcherBase.ksParamEffectiveDate];
+            dParams[getattr(oData, 'ksParam_' + oData.ksIdAttr)] = getattr(oData, oData.ksIdAttr);
+
+            dParams[WuiDispatcherBase.ksParamAction] = getattr(self._oDisp, self._sActionBase + 'Edit');
+            aoActions.append(WuiTmLink('Edit', '', dParams));
+
+            # Add clone operation if available. This uses the same data selection as for showing details.
+            if hasattr(self._oDisp, self._sActionBase + 'Clone'):
+                dParams = self._oDisp.getParameters();
+                dParams[WuiDispatcherBase.ksParamAction] = getattr(self._oDisp, self._sActionBase + 'Clone');
+                aoActions.append(WuiTmLink('Clone', '', dParams));
+
+        elif self._sMode == self.ksMode_Edit:
+            # Details views the details at a given time, so we need either idGen or an effecive date + regular id.
+            dParams = {};
+            if hasattr(oData, 'ksIdGenAttr'):
+                sIdGenParam = getattr(oData, 'ksParam_' + oData.ksIdGenAttr);
+                dParams[sIdGenParam] = getattr(oData, oData.ksIdGenAttr);
+            elif hasattr(oData, 'tsEffective'):
+                dParams[WuiDispatcherBase.ksParamEffectiveDate] = oData.tsEffective;
+                dParams[getattr(oData, 'ksParam_' + oData.ksIdAttr)] = getattr(oData, oData.ksIdAttr);
+            dParams[WuiDispatcherBase.ksParamAction] = getattr(self._oDisp, self._sActionBase + 'Details');
+            aoActions.append(WuiTmLink('Details', '', dParams));
+
+        return aoActions;
+
     def showForm(self, dErrors = None, sErrorMsg = None):
         """
@@ -544,39 +593,19 @@
             sContent = oForm.finalize();
 
+        # Add any post form content.
+        atPostFormContent = self._generatePostFormContent(self._oData);
+        if atPostFormContent is not None and len(atPostFormContent) > 0:
+            for iSection, tSection in enumerate(atPostFormContent):
+                (sSectionTitle, sSectionContent) = tSection;
+                sContent += u'<div id="postform-%d"  class="tmformpostsection">\n' % (iSection,);
+                if sSectionTitle is not None and len(sSectionTitle) > 0:
+                    sContent += '<h3 class="tmformpostheader">%s</h3>\n' % (webutils.escapeElem(sSectionTitle),);
+                sContent += u' <div id="postform-%d-content" class="tmformpostcontent">\n' % (iSection,);
+                sContent += sSectionContent;
+                sContent += u' </div>\n' \
+                            u'</div>\n';
+
         # Add action to the top.
-        aoActions = [];
-        if self._sMode == self.ksMode_Show and self._fEditable:
-            # Remove _idGen and effective date since we're always editing the current data,
-            # and make sure the primary ID is present.
-            dParams = self._oDisp.getParameters();
-            if hasattr(self._oData, 'ksIdGenAttr'):
-                sIdGenParam = getattr(self._oData, 'ksParam_' + self._oData.ksIdGenAttr);
-                if sIdGenParam in dParams:
-                    del dParams[sIdGenParam];
-            if WuiDispatcherBase.ksParamEffectiveDate in dParams:
-                del dParams[WuiDispatcherBase.ksParamEffectiveDate];
-            dParams[getattr(self._oData, 'ksParam_' + self._oData.ksIdAttr)] = getattr(self._oData, self._oData.ksIdAttr);
-
-            dParams[WuiDispatcherBase.ksParamAction] = getattr(self._oDisp, self._sActionBase + 'Edit');
-            aoActions.append(WuiTmLink('Edit', '', dParams));
-
-            # Add clone operation if available. This uses the same data selection as for showing details.
-            if hasattr(self._oDisp, self._sActionBase + 'Clone'):
-                dParams = self._oDisp.getParameters();
-                dParams[WuiDispatcherBase.ksParamAction] = getattr(self._oDisp, self._sActionBase + 'Clone');
-                aoActions.append(WuiTmLink('Clone', '', dParams));
-
-        elif self._sMode == self.ksMode_Edit:
-            # Details views the details at a given time, so we need either idGen or an effecive date + regular id.
-            dParams = {};
-            if hasattr(self._oData, 'ksIdGenAttr'):
-                sIdGenParam = getattr(self._oData, 'ksParam_' + self._oData.ksIdGenAttr);
-                dParams[sIdGenParam] = getattr(self._oData, self._oData.ksIdGenAttr);
-            elif hasattr(self._oData, 'tsEffective'):
-                dParams[WuiDispatcherBase.ksParamEffectiveDate] = self._oData.tsEffective;
-                dParams[getattr(self._oData, 'ksParam_' + self._oData.ksIdAttr)] = getattr(self._oData, self._oData.ksIdAttr);
-            dParams[WuiDispatcherBase.ksParamAction] = getattr(self._oDisp, self._sActionBase + 'Details');
-            aoActions.append(WuiTmLink('Details', '', dParams));
-
+        aoActions = self._generateTopRowFormActions(self._oData);
         if len(aoActions) > 0:
             sActionLinks = '<p>%s</p>' % (' '.join(unicode(oLink) for oLink in aoActions));
Index: /trunk/src/VBox/ValidationKit/testmanager/webui/wuihlpform.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/webui/wuihlpform.py	(revision 61249)
+++ /trunk/src/VBox/ValidationKit/testmanager/webui/wuihlpform.py	(revision 61250)
@@ -124,12 +124,19 @@
     # Non-input stuff.
     #
-    def addNonText(self, sValue, sLabel, sPostHtml = ''):
+    def addNonText(self, sValue, sLabel, sName = 'non-text', sPostHtml = ''):
         """Adds a read-only text input."""
-        self._addLabel('non-text', sLabel, 'string');
+        self._addLabel(sName, sLabel, 'string');
         if sValue is None: sValue = '';
-        return self._add(u'          <p>%s</p>%s\n'
+        return self._add(u'          <p>%s%s</p>\n'
                          u'        </div></div>\n'
                          u'      </li>\n'
                          % (escapeElem(unicode(sValue)), sPostHtml ));
+
+    def addRawHtml(self, sRawHtml, sLabel, sName = 'raw-html'):
+        """Adds a read-only text input."""
+        self._addLabel(sName, sLabel, 'string');
+        self._add(sRawHtml);
+        return self._add(u'        </div></div>\n'
+                         u'      </li>\n');
 
 
Index: /trunk/src/VBox/ValidationKit/testmanager/webui/wuimain.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/webui/wuimain.py	(revision 61249)
+++ /trunk/src/VBox/ValidationKit/testmanager/webui/wuimain.py	(revision 61250)
@@ -36,5 +36,5 @@
 from testmanager.webui.wuicontentbase       import WuiTmLink;
 from testmanager.core.report                import ReportLazyModel, ReportGraphModel, ReportModelBase;
-from testmanager.core.testresults           import TestResultLogic, TestResultFileDataEx;
+from testmanager.core.testresults           import TestResultLogic, TestResultFileDataEx, TestResultData;
 from testmanager.core.base                  import TMExceptionBase, TMTooManyRows;
 from testmanager.core.testset               import TestSetData, TestSetLogic;
@@ -68,5 +68,7 @@
     ksActionResultsGroupedByTestBox     = 'ResultsGroupedByTestBox'
     ksActionResultsGroupedByTestCase    = 'ResultsGroupedByTestCase'
-    ksActionTestResultDetails           = 'TestResultDetails'
+    ksActionTestSetDetails              = 'TestSetDetails';
+    ksActionTestResultDetails           = ksActionTestSetDetails;
+    ksActionTestSetDetailsFromResult    = 'TestSetDetailsFromResult'
     ksActionTestResultFailureDetails    = 'TestResultFailureDetails'
     ksActionTestResultFailureAdd        = 'TestResultFailureAdd'
@@ -162,25 +164,25 @@
     ## Test result period values.
     kaoResultPeriods = [
-        ( '1 hour',   'One hour',    1 ),
-        ( '2 hours',  'Two hours',   2 ),
-        ( '3 hours',  'Three hours', 3 ),
-        ( '6 hours',  'Six hours',   6 ),
+        ( '1 hour',   '1 hour',      1 ),
+        ( '2 hours',  '2 hours',     2 ),
+        ( '3 hours',  '3 hours',     3 ),
+        ( '6 hours',  '6 hours',     6 ),
         ( '12 hours', '12 hours',    12 ),
 
-        ( '1 day',    'One day',     24 ),
-        ( '2 days',   'Two days',    48 ),
-        ( '3 days',   'Three days',  72 ),
-
-        ( '1 week',   'One week',    168 ),
-        ( '2 weeks',  'Two weeks',   336 ),
-        ( '3 weeks',  'Three weeks', 504 ),
-
-        ( '1 month',  'One month',   31 * 24 ),                             # The approx hour count varies with the start date.
-        ( '2 months', 'Two month',   (31 + 31) * 24 ),                      # Using maximum values.
-        ( '3 months', 'Three month', (31 + 30 + 31) * 24 ),
-
-        ( '6 months', 'Six month',   (31 + 31 + 30 + 31 + 30 + 31) * 24 ),
-
-        ( '1 year',   'One year',    365 * 24 ),
+        ( '1 day',    '1 day',       24 ),
+        ( '2 days',   '2 days',      48 ),
+        ( '3 days',   '3 days',      72 ),
+
+        ( '1 week',   '1 week',      168 ),
+        ( '2 weeks',  '2 weeks',     336 ),
+        ( '3 weeks',  '3 weeks',     504 ),
+
+        ( '1 month',  '1 month',     31 * 24 ),                             # The approx hour count varies with the start date.
+        ( '2 months', '2 months',    (31 + 31) * 24 ),                      # Using maximum values.
+        ( '3 months', '3 months',    (31 + 30 + 31) * 24 ),
+
+        ( '6 months', '6 months',    (31 + 31 + 30 + 31 + 30 + 31) * 24 ),
+
+        ( '1 year',   '1 year',      365 * 24 ),
     ];
     ## The default test result period.
@@ -202,51 +204,55 @@
 
         from testmanager.webui.wuitestresult import WuiGroupedResultList;
-        #d[self.ksActionResultsUnGrouped]          = lambda: self._actionResultsListing(TestResultLogic, WuiGroupedResultList)
-        d[self.ksActionResultsUnGrouped]          = lambda: self._actionGroupedResultsListing(
-                                                                TestResultLogic.ksResultsGroupingTypeNone,
-                                                                TestResultLogic,
+        #d[self.ksActionResultsUnGrouped]            = lambda: self._actionResultsListing(TestResultLogic, WuiGroupedResultList)
+        d[self.ksActionResultsUnGrouped]            = lambda: self._actionGroupedResultsListing(
+                                                                  TestResultLogic.ksResultsGroupingTypeNone,
+                                                                  TestResultLogic,
+                                                                  WuiGroupedResultList)
+
+        d[self.ksActionResultsGroupedByTestGroup]   = lambda: self._actionGroupedResultsListing(
+                                                                  TestResultLogic.ksResultsGroupingTypeTestGroup,
+                                                                  TestResultLogic,
+                                                                  WuiGroupedResultList)
+
+        d[self.ksActionResultsGroupedByBuildRev]    = lambda: self._actionGroupedResultsListing(
+                                                                  TestResultLogic.ksResultsGroupingTypeBuildRev,
+                                                                  TestResultLogic,
+                                                                  WuiGroupedResultList)
+
+        d[self.ksActionResultsGroupedByTestBox]     = lambda: self._actionGroupedResultsListing(
+                                                                  TestResultLogic.ksResultsGroupingTypeTestBox,
+                                                                  TestResultLogic,
                                                                 WuiGroupedResultList)
 
-        d[self.ksActionResultsGroupedByTestGroup] = lambda: self._actionGroupedResultsListing(
-                                                                TestResultLogic.ksResultsGroupingTypeTestGroup,
-                                                                TestResultLogic,
-                                                                WuiGroupedResultList)
-
-        d[self.ksActionResultsGroupedByBuildRev]  = lambda: self._actionGroupedResultsListing(
-                                                                TestResultLogic.ksResultsGroupingTypeBuildRev,
-                                                                TestResultLogic,
-                                                                WuiGroupedResultList)
-
-        d[self.ksActionResultsGroupedByTestBox]   = lambda: self._actionGroupedResultsListing(
-                                                                TestResultLogic.ksResultsGroupingTypeTestBox,
-                                                                TestResultLogic,
-                                                                WuiGroupedResultList)
-
-        d[self.ksActionResultsGroupedByTestCase]   = lambda: self._actionGroupedResultsListing(
-                                                                TestResultLogic.ksResultsGroupingTypeTestCase,
-                                                                TestResultLogic,
-                                                                WuiGroupedResultList)
-
-        d[self.ksActionResultsGroupedBySchedGroup] = lambda: self._actionGroupedResultsListing(
-                                                                TestResultLogic.ksResultsGroupingTypeSchedGroup,
-                                                                TestResultLogic,
-                                                                WuiGroupedResultList)
-
-        d[self.ksActionTestResultDetails]          = self._actionTestResultDetails;
-
-        d[self.ksActionTestResultFailureAdd]       = self._actionTestResultFailureAdd;
-        d[self.ksActionTestResultFailureAddPost]   = self._actionTestResultFailureAddPost;
-        d[self.ksActionTestResultFailureDetails]   = self._actionTestResultFailureDetails;
-        d[self.ksActionTestResultFailureEdit]      = self._actionTestResultFailureEdit;
-        d[self.ksActionTestResultFailureEditPost]  = self._actionTestResultFailureEditPost;
-
-        d[self.ksActionViewLog]                 = self.actionViewLog;
-        d[self.ksActionGetFile]                 = self.actionGetFile;
+        d[self.ksActionResultsGroupedByTestCase]    = lambda: self._actionGroupedResultsListing(
+                                                                 TestResultLogic.ksResultsGroupingTypeTestCase,
+                                                                 TestResultLogic,
+                                                                 WuiGroupedResultList)
+
+        d[self.ksActionResultsGroupedBySchedGroup]  = lambda: self._actionGroupedResultsListing(
+                                                                 TestResultLogic.ksResultsGroupingTypeSchedGroup,
+                                                                 TestResultLogic,
+                                                                 WuiGroupedResultList)
+
+        d[self.ksActionTestSetDetails]              = self._actionTestSetDetails;
+        d[self.ksActionTestSetDetailsFromResult]    = self._actionTestSetDetailsFromResult;
+
+        d[self.ksActionTestResultFailureAdd]        = self._actionTestResultFailureAdd;
+        d[self.ksActionTestResultFailureAddPost]    = self._actionTestResultFailureAddPost;
+        d[self.ksActionTestResultFailureDetails]    = self._actionTestResultFailureDetails;
+        d[self.ksActionTestResultFailureEdit]       = self._actionTestResultFailureEdit;
+        d[self.ksActionTestResultFailureEditPost]   = self._actionTestResultFailureEditPost;
+
+        d[self.ksActionViewLog]                     = self.actionViewLog;
+        d[self.ksActionGetFile]                     = self.actionGetFile;
         from testmanager.webui.wuireport import WuiReportSummary, WuiReportSuccessRate, WuiReportFailureReasons;
-        d[self.ksActionReportSummary]           = lambda: self._actionGenericReport(ReportLazyModel, WuiReportSummary);
-        d[self.ksActionReportRate]              = lambda: self._actionGenericReport(ReportLazyModel, WuiReportSuccessRate);
-        d[self.ksActionReportFailureReasons]    = lambda: self._actionGenericReport(ReportLazyModel, WuiReportFailureReasons);
-        d[self.ksActionGraphWiz]                = self._actionGraphWiz;
-        d[self.ksActionVcsHistoryTooltip]       = self._actionVcsHistoryTooltip;
+        d[self.ksActionReportSummary]               = lambda: self._actionGenericReport(ReportLazyModel, WuiReportSummary);
+        d[self.ksActionReportRate]                  = lambda: self._actionGenericReport(ReportLazyModel, WuiReportSuccessRate);
+        d[self.ksActionReportFailureReasons]        = lambda: self._actionGenericReport(ReportLazyModel, WuiReportFailureReasons);
+        d[self.ksActionGraphWiz]                    = self._actionGraphWiz;
+        d[self.ksActionVcsHistoryTooltip]           = self._actionVcsHistoryTooltip;
+
+        # Legacy.
+        d['TestResultDetails']                      = d[self.ksActionTestSetDetails];
 
 
@@ -389,24 +395,24 @@
             ('+0000-00-00 00:00:00.00', 'Now', ' title="Present Day. Present Time."'), # lain :)
 
-            ('-0000-00-00 01:00:00.00', 'One hour ago', ''),
-            ('-0000-00-00 02:00:00.00', 'Two hours ago', ''),
-            ('-0000-00-00 03:00:00.00', 'Three hours ago', ''),
-
-            ('-0000-00-01 00:00:00.00', 'One day ago', ''),
-            ('-0000-00-02 00:00:00.00', 'Two days ago', ''),
-            ('-0000-00-03 00:00:00.00', 'Three days ago', ''),
-
-            ('-0000-00-07 00:00:00.00', 'One week ago', ''),
-            ('-0000-00-14 00:00:00.00', 'Two weeks ago', ''),
-            ('-0000-00-21 00:00:00.00', 'Three weeks ago', ''),
-
-            ('-0000-01-00 00:00:00.00', 'One month ago', ''),
-            ('-0000-02-00 00:00:00.00', 'Two months ago', ''),
-            ('-0000-03-00 00:00:00.00', 'Three months ago', ''),
-            ('-0000-04-00 00:00:00.00', 'Four months ago', ''),
-            ('-0000-05-00 00:00:00.00', 'Five months ago', ''),
+            ('-0000-00-00 01:00:00.00', '1 hour ago', ''),
+            ('-0000-00-00 02:00:00.00', '2 hours ago', ''),
+            ('-0000-00-00 03:00:00.00', '3 hours ago', ''),
+
+            ('-0000-00-01 00:00:00.00', '1 day ago', ''),
+            ('-0000-00-02 00:00:00.00', '2 days ago', ''),
+            ('-0000-00-03 00:00:00.00', '3 days ago', ''),
+
+            ('-0000-00-07 00:00:00.00', '1 week ago', ''),
+            ('-0000-00-14 00:00:00.00', '2 weeks ago', ''),
+            ('-0000-00-21 00:00:00.00', '3 weeks ago', ''),
+
+            ('-0000-01-00 00:00:00.00', '1 month ago', ''),
+            ('-0000-02-00 00:00:00.00', '2 months ago', ''),
+            ('-0000-03-00 00:00:00.00', '3 months ago', ''),
+            ('-0000-04-00 00:00:00.00', '4 months ago', ''),
+            ('-0000-05-00 00:00:00.00', '5 months ago', ''),
             ('-0000-06-00 00:00:00.00', 'Half a year ago', ''),
 
-            ('-0001-00-00 00:00:00.00', 'One year ago', ''),
+            ('-0001-00-00 00:00:00.00', '1 year ago', ''),
         ]
         fSelected = False;
@@ -842,10 +848,9 @@
         return WuiDispatcherBase._generatePage(self)
 
-    def _actionTestResultDetails(self):
+    def _actionTestSetDetailsCommon(self, idTestSet):
         """Show test case execution result details."""
         from testmanager.webui.wuitestresult import WuiTestResult;
 
         self._sTemplate = 'template-details.html';
-        idTestSet = self.getIntParam(TestSetData.ksParam_idTestSet);
         self._checkForUnknownParameters()
 
@@ -878,4 +883,16 @@
         return True
 
+    def _actionTestSetDetails(self):
+        """Show test case execution result details."""
+        idTestSet = self.getIntParam(TestSetData.ksParam_idTestSet);
+        return self._actionTestSetDetailsCommon(idTestSet);
+
+    def _actionTestSetDetailsFromResult(self):
+        """Show test case execution result details."""
+        idTestResult = self.getIntParam(TestSetData.ksParam_idTestResult);
+        oTestResultData = TestResultData().initFromDbWithId(self._oDb, idTestResult);
+        return self._actionTestSetDetailsCommon(oTestResultData.idTestSet);
+
+
     def _actionTestResultFailureAdd(self):
         """ Pro forma. """
@@ -912,7 +929,4 @@
         from testmanager.core.testresults import TestResultFailureLogic, TestResultFailureData;
         from testmanager.webui.wuitestresultfailure import WuiTestResultFailure;
-        if self.ksParamRedirectTo not in self._dParams:
-            raise WuiException('Missing parameter ' + self.ksParamRedirectTo);
-
         return self._actionGenericFormEditPost(TestResultFailureData, TestResultFailureLogic,
                                                WuiTestResultFailure, self.ksActionResultsUnGrouped);
Index: /trunk/src/VBox/ValidationKit/testmanager/webui/wuitestresult.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/webui/wuitestresult.py	(revision 61249)
+++ /trunk/src/VBox/ValidationKit/testmanager/webui/wuitestresult.py	(revision 61250)
@@ -32,44 +32,22 @@
 
 # Validation Kit imports.
-from testmanager.webui.wuicontentbase   import WuiContentBase, WuiListContentBase, WuiHtmlBase, WuiTmLink, WuiLinkBase, \
-                                               WuiSvnLink, WuiSvnLinkWithTooltip, WuiBuildLogLink, WuiRawHtml;
-from testmanager.webui.wuimain          import WuiMain;
-from testmanager.webui.wuihlpform       import WuiHlpForm;
-from testmanager.core.failurereason     import FailureReasonData, FailureReasonLogic;
-from testmanager.core.report            import ReportGraphModel;
-from testmanager.core.testbox           import TestBoxData;
-from testmanager.core.testcase          import TestCaseData;
-from testmanager.core.testset           import TestSetData;
-from testmanager.core.testgroup         import TestGroupData;
-from testmanager.core.testresults       import TestResultFailureData;
-from testmanager.core.build             import BuildData;
-from testmanager.core                   import db;
-from testmanager                        import config;
-from common                             import webutils, utils;
-
-
-class WuiFailureReasonDetailsLink(WuiTmLink):
-    """ Short link to a failure reason. """
-    def __init__(self, idFailureReason, sName = WuiContentBase.ksShortDetailsLink, sTitle = None, fBracketed = None):
-        if fBracketed is None:
-            fBracketed = len(sName) > 2;
-        from testmanager.webui.wuiadmin import WuiAdmin;
-        WuiTmLink.__init__(self, sName = sName,
-                           sUrlBase = WuiAdmin.ksScriptName,
-                           dParams = { WuiAdmin.ksParamAction: WuiAdmin.ksActionFailureReasonDetails,
-                                       FailureReasonData.ksParam_idFailureReason: idFailureReason, },
-                           fBracketed = fBracketed);
-        self.idFailureReason = idFailureReason;
-
-class WuiFailureReasonAddLink(WuiTmLink):
-    """ Link for adding a failure reason. """
-    def __init__(self, sName = WuiContentBase.ksShortAddLink, sTitle = None, fBracketed = None):
-        if fBracketed is None:
-            fBracketed = len(sName) > 2;
-        from testmanager.webui.wuiadmin import WuiAdmin;
-        WuiTmLink.__init__(self, sName = sName,
-                           sUrlBase = WuiAdmin.ksScriptName,
-                           dParams = { WuiAdmin.ksParamAction: WuiAdmin.ksActionFailureReasonAdd, },
-                           fBracketed = fBracketed);
+from testmanager.webui.wuicontentbase           import WuiContentBase, WuiListContentBase, WuiHtmlBase, WuiTmLink, WuiLinkBase, \
+                                                       WuiSvnLink, WuiSvnLinkWithTooltip, WuiBuildLogLink, WuiRawHtml;
+from testmanager.webui.wuimain                  import WuiMain;
+from testmanager.webui.wuihlpform               import WuiHlpForm;
+from testmanager.webui.wuiadminfailurereason    import WuiFailureReasonAddLink, WuiFailureReasonDetailsLink;
+from testmanager.webui.wuitestresultfailure     import WuiTestResultFailureDetailsLink;
+from testmanager.core.failurereason             import FailureReasonData, FailureReasonLogic;
+from testmanager.core.report                    import ReportGraphModel;
+from testmanager.core.testbox                   import TestBoxData;
+from testmanager.core.testcase                  import TestCaseData;
+from testmanager.core.testset                   import TestSetData;
+from testmanager.core.testgroup                 import TestGroupData;
+from testmanager.core.testresults               import TestResultFailureData;
+from testmanager.core.build                     import BuildData;
+from testmanager.core                           import db;
+from testmanager                                import config;
+from common                                     import webutils, utils;
+
 
 
@@ -401,11 +379,19 @@
                 oForm.addComboBox(TestResultFailureData.ksParam_idFailureReason, oData.idFailureReason, 'Reason',
                                   aoFailureReasons,
-                                  sPostHtml = WuiFailureReasonDetailsLink(oData.idFailureReason).toHtml());
+                                  sPostHtml = u' ' + WuiFailureReasonDetailsLink(oData.idFailureReason).toHtml()
+                                            + u' ' + WuiFailureReasonAddLink('New', fBracketed = False).toHtml());
                 oForm.addMultilineText(TestResultFailureData.ksParam_sComment, oData.sComment, 'Comment')
 
-                oForm.addNonText('%s (%s), %s' % (oData.oAuthor.sUsername, oData.oAuthor.sUsername, self.formatTsShort(oData.tsEffective)), 'Sheriff');
+                oForm.addNonText(u'%s (%s), %s'
+                                 % ( oData.oAuthor.sUsername, oData.oAuthor.sUsername,
+                                     self.formatTsShort(oData.tsEffective),),
+                                 'Sheriff',
+                                 sPostHtml = ' ' + WuiTestResultFailureDetailsLink(oData.idTestResult, "Show Details").toHtml() )
+
                 oForm.addTextHidden(TestResultFailureData.ksParam_tsEffective, oData.tsEffective);
                 oForm.addTextHidden(TestResultFailureData.ksParam_tsExpire, oData.tsExpire);
                 oForm.addTextHidden(TestResultFailureData.ksParam_uidAuthor, oData.uidAuthor);
+                oForm.addSubmit('Change Reason');
+
             else:
                 oForm.addComboBox(TestResultFailureData.ksParam_idFailureReason, -1, 'Reason', aoFailureReasons,
@@ -415,6 +401,6 @@
                 oForm.addTextHidden(TestResultFailureData.ksParam_tsExpire, '');
                 oForm.addTextHidden(TestResultFailureData.ksParam_uidAuthor, '');
-
-            oForm.addSubmit('Change Reason');
+                oForm.addSubmit('Add Reason');
+
             sHtml += oForm.finalize();
         return sHtml;
Index: /trunk/src/VBox/ValidationKit/testmanager/webui/wuitestresultfailure.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/webui/wuitestresultfailure.py	(revision 61249)
+++ /trunk/src/VBox/ValidationKit/testmanager/webui/wuitestresultfailure.py	(revision 61250)
@@ -30,7 +30,25 @@
 
 # Validation Kit imports.
-from testmanager.webui.wuicontentbase   import WuiFormContentBase;
-from testmanager.core.testresults       import TestResultFailureData;
-from testmanager.core.failurereason     import FailureReasonLogic;
+from testmanager.webui.wuicontentbase           import WuiFormContentBase, WuiTmLink;
+from testmanager.webui.wuimain                  import WuiMain;
+from testmanager.webui.wuiadminfailurereason    import WuiFailureReasonDetailsLink, WuiFailureReasonAddLink;
+from testmanager.core.testresults               import TestResultFailureData;
+from testmanager.core.testset                   import TestSetData;
+from testmanager.core.failurereason             import FailureReasonLogic;
+
+
+
+class WuiTestResultFailureDetailsLink(WuiTmLink):
+    """ Link for adding a failure reason. """
+    def __init__(self, idTestResult, sName = u'\u2397', sTitle = None, fBracketed = None):
+        if fBracketed is None:
+            fBracketed = len(sName) > 2;
+        WuiTmLink.__init__(self, sName = sName,
+                           sUrlBase = WuiMain.ksScriptName,
+                           dParams = { WuiMain.ksParamAction: WuiMain.ksActionTestResultFailureDetails,
+                                       TestResultFailureData.ksParam_idTestResult: idTestResult, },
+                           fBracketed = fBracketed);
+        self.idTestResult = idTestResult;
+
 
 
@@ -53,6 +71,10 @@
 
         aoFailureReasons = FailureReasonLogic(self._oDisp.getDb()).fetchForCombo('Todo: Figure out why');
+        sPostHtml = '';
+        if oData.idFailureReason is not None and oData.idFailureReason >= 0:
+            sPostHtml += u' ' + WuiFailureReasonDetailsLink(oData.idFailureReason).toHtml();
+        sPostHtml += u' ' + WuiFailureReasonAddLink('New', fBracketed = False).toHtml();
         oForm.addComboBox(TestResultFailureData.ksParam_idFailureReason, oData.idFailureReason,
-                          'Reason', aoFailureReasons)
+                          'Reason', aoFailureReasons, sPostHtml = sPostHtml);
         oForm.addMultilineText(TestResultFailureData.ksParam_sComment,   oData.sComment,     'Comment');
         oForm.addIntRO(      TestResultFailureData.ksParam_idTestResult, oData.idTestResult, 'Test Result ID');
@@ -64,2 +86,16 @@
         return True;
 
+    def _generateTopRowFormActions(self, oData):
+        """
+        We add a way to get back to the test set to the actions.
+        """
+        aoActions = super(WuiTestResultFailure, self)._generateTopRowFormActions(oData);
+        if oData and oData.idTestResult is not None and oData.idTestResult > 0:
+            aoActions.append(WuiTmLink('Associated Test Set', WuiMain.ksScriptName,
+                                       { WuiMain.ksParamAction: WuiMain.ksActionTestSetDetailsFromResult,
+                                         TestSetData.ksParam_idTestResult: oData.idTestResult }
+                                       ));
+        return aoActions;
+
+
+
