add the option to include verify/repair/cleanups in the job table on the host detail page.
* added RPC get_host_queue_entries_and_special_tasks, which returns both HQEs and SpecialTasks for a host in a carefully interleaved manner.
* added appropriate frontend unit tests
* added support to HostDetailView to include these entries, but have them not be selectable.  this required changes to SelectionManager.  I also added a utility method to Utils for opening a new window, and changed all sites that do that to call the new method.

Signed-off-by: Steve Howard <showard@google.com>


git-svn-id: http://test.kernel.org/svn/autotest/trunk@3385 592f7852-d20e-0410-864c-8624ca9c26a4
diff --git a/frontend/afe/model_logic.py b/frontend/afe/model_logic.py
index 32b4898..c94233b 100644
--- a/frontend/afe/model_logic.py
+++ b/frontend/afe/model_logic.py
@@ -602,6 +602,7 @@
          DB layer documentation)
          -extra_where: extra WHERE clause to append
         """
+        filter_data = dict(filter_data) # copy so we don't mutate the original
         query_start = filter_data.pop('query_start', None)
         query_limit = filter_data.pop('query_limit', None)
         if query_start and not query_limit:
diff --git a/frontend/afe/models.py b/frontend/afe/models.py
index 4aa7547..48cbcf7 100644
--- a/frontend/afe/models.py
+++ b/frontend/afe/models.py
@@ -840,6 +840,13 @@
         self._check_for_updated_attributes()
 
 
+    def execution_path(self):
+        """
+        Path to this entry's results (relative to the base results directory).
+        """
+        return self.execution_subdir
+
+
     def host_or_metahost_name(self):
         if self.host:
             return self.host.hostname
@@ -983,17 +990,39 @@
                                             null=False)
     is_active = dbmodels.BooleanField(default=False, blank=False, null=False)
     is_complete = dbmodels.BooleanField(default=False, blank=False, null=False)
-    time_started = dbmodels.DateTimeField(null=True)
+    time_started = dbmodels.DateTimeField(null=True, blank=True)
     queue_entry = dbmodels.ForeignKey(HostQueueEntry, blank=True, null=True)
 
     objects = model_logic.ExtendedManager()
 
 
     def execution_path(self):
+        """@see HostQueueEntry.execution_path()"""
         return 'hosts/%s/%s-%s' % (self.host.hostname, self.id,
                                    self.task.lower())
 
 
+    # property to emulate HostQueueEntry.status
+    @property
+    def status(self):
+        """
+        Return a host queue entry status appropriate for this task.  Although
+        SpecialTasks are not HostQueueEntries, it is helpful to the user to
+        present similar statuses.
+        """
+        if self.is_complete:
+            return HostQueueEntry.Status.COMPLETED
+        if self.is_active:
+            return HostQueueEntry.Status.RUNNING
+        return HostQueueEntry.Status.QUEUED
+
+
+    # property to emulate HostQueueEntry.started_on
+    @property
+    def started_on(self):
+        return self.time_started
+
+
     @classmethod
     def schedule_special_task(cls, hosts, task):
         """\
diff --git a/frontend/afe/models_test.py b/frontend/afe/models_test.py
index bb20103..7094881 100644
--- a/frontend/afe/models_test.py
+++ b/frontend/afe/models_test.py
@@ -16,12 +16,26 @@
         self._frontend_common_teardown()
 
 
-    def test_execution_path(self):
-        task = models.SpecialTask.objects.create(
+    def _create_task(self):
+        return models.SpecialTask.objects.create(
                 host=self.hosts[0], task=models.SpecialTask.Task.VERIFY)
 
+
+    def test_execution_path(self):
+        task = self._create_task()
         self.assertEquals(task.execution_path(), 'hosts/host1/1-verify')
 
 
+    def test_status(self):
+        task = self._create_task()
+        self.assertEquals(task.status, 'Queued')
+
+        task.update_object(is_active=True)
+        self.assertEquals(task.status, 'Running')
+
+        task.update_object(is_active=False, is_complete=True)
+        self.assertEquals(task.status, 'Completed')
+
+
 if __name__ == '__main__':
     unittest.main()
diff --git a/frontend/afe/rpc_interface.py b/frontend/afe/rpc_interface.py
index 3184665..5712f0e 100644
--- a/frontend/afe/rpc_interface.py
+++ b/frontend/afe/rpc_interface.py
@@ -650,6 +650,40 @@
     return float(complete_count) / total_count
 
 
+# support for host detail view
+
+def get_host_queue_entries_and_special_tasks(hostname, query_start=None,
+                                             query_limit=None):
+    """
+    @returns an interleaved list of HostQueueEntries and SpecialTasks,
+            in approximate run order.  each dict contains keys for type, host,
+            job, status, started_on, execution_path, and ID.
+    """
+    total_limit = None
+    if query_limit is not None:
+        total_limit = query_start + query_limit
+    filter_data = {'host__hostname': hostname,
+                   'query_limit': total_limit,
+                   'sort_by': ['-id']}
+
+    queue_entries = list(models.HostQueueEntry.query_objects(filter_data))
+    special_tasks = list(models.SpecialTask.query_objects(filter_data))
+
+    interleaved_entries = rpc_utils.interleave_entries(queue_entries,
+                                                       special_tasks)
+    if query_start is not None:
+        interleaved_entries = interleaved_entries[query_start:]
+    if query_limit is not None:
+        interleaved_entries = interleaved_entries[:query_limit]
+    return rpc_utils.prepare_for_serialization(interleaved_entries)
+
+
+def get_num_host_queue_entries_and_special_tasks(hostname):
+    filter_data = {'host__hostname': hostname}
+    return (models.HostQueueEntry.query_count(filter_data)
+            + models.SpecialTask.query_count(filter_data))
+
+
 # recurring run
 
 def get_recurring(**filter_data):
diff --git a/frontend/afe/rpc_interface_unittest.py b/frontend/afe/rpc_interface_unittest.py
index 8e3ac25..a1cab5e 100644
--- a/frontend/afe/rpc_interface_unittest.py
+++ b/frontend/afe/rpc_interface_unittest.py
@@ -37,7 +37,7 @@
 
 
     def test_get_jobs_summary(self):
-        job = self._create_job(xrange(3))
+        job = self._create_job(hosts=xrange(1, 4))
         entries = list(job.hostqueueentry_set.all())
         entries[1].status = _hqe_status.FAILED
         entries[1].save()
@@ -61,5 +61,58 @@
         self.assertEquals(host.aclgroup_set.count(), 0)
 
 
+    def _common_entry_check(self, entry_dict):
+        self.assertEquals(entry_dict['host']['hostname'], 'host1')
+        self.assertEquals(entry_dict['job']['id'], 2)
+
+
+
+    def test_get_host_queue_entries_and_special_tasks(self):
+        host = self.hosts[0]
+
+        job1 = self._create_job(hosts=[1])
+        job2 = self._create_job(hosts=[1])
+
+        entry1 = job1.hostqueueentry_set.all()[0]
+        entry1.update_object(started_on=datetime.datetime(2009, 1, 2),
+                             execution_subdir='1-myuser/host1')
+        entry2 = job2.hostqueueentry_set.all()[0]
+        entry2.update_object(started_on=datetime.datetime(2009, 1, 3),
+                             execution_subdir='2-myuser/host1')
+
+        task1 = models.SpecialTask.objects.create(
+                host=host, task=models.SpecialTask.Task.VERIFY,
+                time_started=datetime.datetime(2009, 1, 1), # ran before job 1
+                is_complete=True)
+        task2 = models.SpecialTask.objects.create(
+                host=host, task=models.SpecialTask.Task.VERIFY,
+                queue_entry=entry2, # ran with job 2
+                is_active=True)
+        task3 = models.SpecialTask.objects.create(
+                host=host, task=models.SpecialTask.Task.VERIFY) # not yet run
+
+        entries_and_tasks = (
+                rpc_interface.get_host_queue_entries_and_special_tasks('host1'))
+
+        paths = [entry['execution_path'] for entry in entries_and_tasks]
+        self.assertEquals(paths, ['hosts/host1/3-verify',
+                                  '2-myuser/host1',
+                                  'hosts/host1/2-verify',
+                                  '1-myuser/host1',
+                                  'hosts/host1/1-verify'])
+
+        verify2 = entries_and_tasks[2]
+        self._common_entry_check(verify2)
+        self.assertEquals(verify2['type'], 'Verify')
+        self.assertEquals(verify2['status'], 'Running')
+        self.assertEquals(verify2['execution_path'], 'hosts/host1/2-verify')
+
+        entry2 = entries_and_tasks[1]
+        self._common_entry_check(entry2)
+        self.assertEquals(entry2['type'], 'Job')
+        self.assertEquals(entry2['status'], 'Queued')
+        self.assertEquals(entry2['started_on'], '2009-01-03 00:00:00')
+
+
 if __name__ == '__main__':
     unittest.main()
diff --git a/frontend/afe/rpc_utils.py b/frontend/afe/rpc_utils.py
index a82345e..da34daa 100644
--- a/frontend/afe/rpc_utils.py
+++ b/frontend/afe/rpc_utils.py
@@ -463,3 +463,88 @@
     # error but should not trip up the RPC interface.  monitor_db_cleanup
     # deals with it.  This just returns the first one found.
     return platform, atomic_group_name
+
+
+# support for get_host_queue_entries_and_special_tasks()
+
+def _common_entry_to_dict(entry, type, job_dict):
+    return dict(type=type,
+                host=entry.host.get_object_dict(),
+                job=job_dict,
+                execution_path=entry.execution_path(),
+                status=entry.status,
+                started_on=entry.started_on,
+                id=entry.id)
+
+
+def _special_task_to_dict(special_task):
+    job_dict = None
+    if special_task.queue_entry:
+        job_dict = special_task.queue_entry.job.get_object_dict()
+    return _common_entry_to_dict(special_task, special_task.task, job_dict)
+
+
+def _queue_entry_to_dict(queue_entry):
+    return _common_entry_to_dict(queue_entry, 'Job',
+                                 queue_entry.job.get_object_dict())
+
+
+def _compute_next_job_for_tasks(queue_entries, special_tasks):
+    """
+    For each task, try to figure out the next job that ran after that task.
+    This is done using two pieces of information:
+    * if the task has a queue entry, we can use that entry's job ID.
+    * if the task has a time_started, we can try to compare that against the
+      started_on field of queue_entries. this isn't guaranteed to work perfectly
+      since queue_entries may also have null started_on values.
+    * if the task has neither, or if use of time_started fails, just use the
+      last computed job ID.
+    """
+    next_job_id = None # most recently computed next job
+    hqe_index = 0 # index for scanning by started_on times
+    for task in special_tasks:
+        if task.queue_entry:
+            next_job_id = task.queue_entry.job.id
+        elif task.time_started is not None:
+            for queue_entry in queue_entries[hqe_index:]:
+                if queue_entry.started_on is None:
+                    continue
+                if queue_entry.started_on < task.time_started:
+                    break
+                next_job_id = queue_entry.job.id
+
+        task.next_job_id = next_job_id
+
+        # advance hqe_index to just after next_job_id
+        if next_job_id is not None:
+            for queue_entry in queue_entries[hqe_index:]:
+                if queue_entry.job.id < next_job_id:
+                    break
+                hqe_index += 1
+
+
+def interleave_entries(queue_entries, special_tasks):
+    """
+    Both lists should be ordered by descending ID.
+    """
+    _compute_next_job_for_tasks(queue_entries, special_tasks)
+
+    # start with all special tasks that've run since the last job
+    interleaved_entries = []
+    for task in special_tasks:
+        if task.next_job_id is not None:
+            break
+        interleaved_entries.append(_special_task_to_dict(task))
+
+    # now interleave queue entries with the remaining special tasks
+    special_task_index = len(interleaved_entries)
+    for queue_entry in queue_entries:
+        interleaved_entries.append(_queue_entry_to_dict(queue_entry))
+        # add all tasks that ran between this job and the previous one
+        for task in special_tasks[special_task_index:]:
+            if task.next_job_id < queue_entry.job.id:
+                break
+            interleaved_entries.append(_special_task_to_dict(task))
+            special_task_index += 1
+
+    return interleaved_entries
diff --git a/frontend/client/src/autotest/afe/HostDetailView.java b/frontend/client/src/autotest/afe/HostDetailView.java
index c53890c..14ac1ef 100644
--- a/frontend/client/src/autotest/afe/HostDetailView.java
+++ b/frontend/client/src/autotest/afe/HostDetailView.java
@@ -12,6 +12,7 @@
 import autotest.common.table.DataSource.DataCallback;
 import autotest.common.table.DataSource.SortDirection;
 import autotest.common.table.DynamicTable.DynamicTableListener;
+import autotest.common.table.SelectionManager.SelectableRowFilter;
 import autotest.common.ui.ContextMenu;
 import autotest.common.ui.DetailView;
 import autotest.common.ui.NotifyManager;
@@ -23,14 +24,17 @@
 import com.google.gwt.user.client.Command;
 import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.CheckBox;
 import com.google.gwt.user.client.ui.ClickListener;
 import com.google.gwt.user.client.ui.RootPanel;
 import com.google.gwt.user.client.ui.Widget;
 
-public class HostDetailView extends DetailView implements DataCallback, TableActionsListener {
+public class HostDetailView extends DetailView 
+                            implements DataCallback, TableActionsListener, SelectableRowFilter {
     private static final String[][] HOST_JOBS_COLUMNS = {
-            {DataTable.WIDGET_COLUMN, ""}, {"job_id", "Job ID"}, {"job_owner", "Job Owner"}, 
-            {"job_name", "Job Name"}, {"status", "Status"}
+            {DataTable.WIDGET_COLUMN, ""}, {"type", "Type"}, {"job_id", "Job ID"}, 
+            {"job_owner", "Job Owner"}, {"job_name", "Job Name"}, {"started_on", "Time started"},
+            {"status", "Status"}
     };
     public static final int JOBS_PER_PAGE = 20;
     
@@ -39,35 +43,84 @@
     }
     
     static class HostJobsTable extends DynamicTable {
-        public HostJobsTable(String[][] columns, DataSource dataSource) {
-            super(columns, dataSource);
+        private static final DataSource normalDataSource = 
+            new RpcDataSource("get_host_queue_entries", "get_num_host_queue_entries");
+        private static final DataSource dataSourceWithSpecialTasks = 
+            new RpcDataSource("get_host_queue_entries_and_special_tasks",
+                              "get_num_host_queue_entries_and_special_tasks");
+
+        private SimpleFilter hostFilter = new SimpleFilter();
+        private String hostname;
+
+        public HostJobsTable() {
+            super(HOST_JOBS_COLUMNS, normalDataSource);
+            addFilter(hostFilter);
+        }
+        
+        public void setHostname(String hostname) {
+            this.hostname = hostname;
+            updateFilter();
+        }
+
+        private void updateFilter() {
+            String key;
+            if (getDataSource() == normalDataSource) {
+                key = "host__hostname";
+                sortOnColumn("job_id", SortDirection.DESCENDING);
+            } else {
+                key = "hostname";
+                clearSorts();
+            }
+
+            hostFilter.clear();
+            hostFilter.setParameter(key, new JSONString(hostname));
+        }
+        
+        public void setSpecialTasksEnabled(boolean enabled) {
+            if (enabled) {
+                setDataSource(dataSourceWithSpecialTasks);
+            } else {
+                setDataSource(normalDataSource);
+            }
+            
+            updateFilter();
         }
 
         @Override
         protected void preprocessRow(JSONObject row) {
             JSONObject job = row.get("job").isObject();
-            int jobId = (int) job.get("id").isNumber().doubleValue();
-            row.put("job_id", new JSONString(Integer.toString(jobId)));
-            row.put("job_owner", job.get("owner"));
-            row.put("job_name", job.get("name"));
+            JSONString blank = new JSONString("");
+            JSONString jobId = blank, owner = blank, name = blank;
+            if (job != null) {
+                int id = (int) job.get("id").isNumber().doubleValue();
+                jobId = new JSONString(Integer.toString(id));
+                owner = job.get("owner").isString();
+                name = job.get("name").isString();
+            }
+
+            row.put("job_id", jobId);
+            row.put("job_owner", owner);
+            row.put("job_name", name);
+
+            // get_host_queue_entries() doesn't return type, so fill it in for consistency
+            if (!row.containsKey("type")) {
+                row.put("type", new JSONString("Job"));
+            }
         }
     }
     
-    protected String hostname = "";
-    protected DataSource hostDataSource = new HostDataSource();
-    protected DynamicTable jobsTable = 
-        new HostJobsTable(HOST_JOBS_COLUMNS, 
-                          new RpcDataSource("get_host_queue_entries", 
-                                            "get_num_host_queue_entries"));
-    protected TableDecorator tableDecorator = new TableDecorator(jobsTable);
-    protected SimpleFilter hostFilter = new SimpleFilter();
-    protected HostDetailListener listener = null;
+    private String hostname = "";
+    private DataSource hostDataSource = new HostDataSource();
+    private HostJobsTable jobsTable = new HostJobsTable();
+    private TableDecorator tableDecorator = new TableDecorator(jobsTable);
+    private HostDetailListener listener = null;
     private SelectionManager selectionManager;
     
     private JSONObject currentHostObject;
     
     private Button lockButton = new Button();
     private Button reverifyButton = new Button("Reverify");
+    private CheckBox showSpecialTasks = new CheckBox();
 
     public HostDetailView(HostDetailListener listener) {
         this.listener = listener;
@@ -144,7 +197,7 @@
         DOM.setElementProperty(DOM.getElementById("view_host_logs_link"), "href",
                 getLogLink(hostname));
         
-        hostFilter.setParameter("host__hostname", new JSONString(hostname));
+        jobsTable.setHostname(hostname);
         jobsTable.refresh();
     }
 
@@ -157,24 +210,37 @@
         super.initialize();
         
         jobsTable.setRowsPerPage(JOBS_PER_PAGE);
-        jobsTable.sortOnColumn("job_id", SortDirection.DESCENDING);
-        jobsTable.addFilter(hostFilter);
         jobsTable.setClickable(true);
         jobsTable.addListener(new DynamicTableListener() {
             public void onRowClicked(int rowIndex, JSONObject row) {
-                JSONObject job = row.get("job").isObject();
-                int jobId = (int) job.get("id").isNumber().doubleValue();
-                listener.onJobSelected(jobId);
+                if (isJobRow(row)) {
+                    JSONObject job = row.get("job").isObject();
+                    int jobId = (int) job.get("id").isNumber().doubleValue();
+                    listener.onJobSelected(jobId);
+                } else {
+                    String resultsPath = "/results/" 
+                                         + Utils.jsonToString(row.get("execution_path"));
+                    Utils.openUrlInNewWindow(resultsPath);
+                }
             }
 
             public void onTableRefreshed() {}
         });
         tableDecorator.addPaginators();
         selectionManager = tableDecorator.addSelectionManager(false);
+        selectionManager.setSelectableRowFilter(this);
         jobsTable.setWidgetFactory(selectionManager);
         tableDecorator.addTableActionsPanel(this, true);
+        tableDecorator.addControl("Show verifies, repairs and cleanups", showSpecialTasks);
         RootPanel.get("view_host_jobs_table").add(tableDecorator);
         
+        showSpecialTasks.addClickListener(new ClickListener() {
+            public void onClick(Widget sender) {
+                jobsTable.setSpecialTasksEnabled(showSpecialTasks.isChecked());
+                jobsTable.refresh();
+            }
+        });
+        
         lockButton.addClickListener(new ClickListener() {
             public void onClick(Widget sender) {
                boolean locked = currentHostObject.get("locked").isBoolean().booleanValue();
@@ -239,4 +305,13 @@
             }
         });
     }
+    
+    private boolean isJobRow(JSONObject row) {
+        String type = Utils.jsonToString(row.get("type"));
+        return type.equals("Job");
+    }
+
+    public boolean isRowSelectable(JSONObject row) {
+        return isJobRow(row);
+    }
 }
diff --git a/frontend/client/src/autotest/common/Utils.java b/frontend/client/src/autotest/common/Utils.java
index 0dc7c50..00e5439 100644
--- a/frontend/client/src/autotest/common/Utils.java
+++ b/frontend/client/src/autotest/common/Utils.java
@@ -6,6 +6,7 @@
 import com.google.gwt.json.client.JSONObject;
 import com.google.gwt.json.client.JSONString;
 import com.google.gwt.json.client.JSONValue;
+import com.google.gwt.user.client.Window;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -240,4 +241,8 @@
             destination.put(key, source.get(key));
         }
     }
+
+    public static void openUrlInNewWindow(String url) {
+        Window.open(url, "_blank", "");
+    }
 }
diff --git a/frontend/client/src/autotest/common/table/DynamicTable.java b/frontend/client/src/autotest/common/table/DynamicTable.java
index a10eb62..e95e479 100644
--- a/frontend/client/src/autotest/common/table/DynamicTable.java
+++ b/frontend/client/src/autotest/common/table/DynamicTable.java
@@ -67,7 +67,7 @@
     
     public DynamicTable(String[][] columns, DataSource dataSource) {
         super(columns);
-        this.dataSource = dataSource;
+        setDataSource(dataSource);
     }
     
     // SORTING
@@ -145,6 +145,11 @@
         sortOnColumn(columnField, SortDirection.ASCENDING);
     }
     
+    public void clearSorts() {
+        sortColumns.clear();
+        updateSortIndicators();
+    }
+    
     // PAGINATION
     
     /**
@@ -263,6 +268,10 @@
         return dataSource;
     }
     
+    public void setDataSource(DataSource dataSource) {
+        this.dataSource = dataSource;
+    }
+    
     
     // INPUT
     
diff --git a/frontend/client/src/autotest/common/table/SelectionManager.java b/frontend/client/src/autotest/common/table/SelectionManager.java
index 517e879..e402edd 100644
--- a/frontend/client/src/autotest/common/table/SelectionManager.java
+++ b/frontend/client/src/autotest/common/table/SelectionManager.java
@@ -25,32 +25,52 @@
  */
 public class SelectionManager implements TableWidgetFactory, TableWidgetClickListener,
                                          SelectionPanelListener {
-    protected Set<JSONObject> selectedObjects = new JSONObjectSet<JSONObject>();
-    protected boolean selectOnlyOne = false;
-    protected DataTable attachedTable;
-    protected List<SelectionListener> listeners =
-        new ArrayList<SelectionListener>();
+    private Set<JSONObject> selectedObjects = new JSONObjectSet<JSONObject>();
+    private boolean selectOnlyOne = false;
+    private DataTable attachedTable;
+    private List<SelectionListener> listeners = new ArrayList<SelectionListener>();
+    private SelectableRowFilter selectableRowFilter;
     
     public interface SelectionListener {
         public void onAdd(Collection<JSONObject> objects);
         public void onRemove(Collection<JSONObject> objects);
     }
     
+    public interface SelectableRowFilter {
+        public boolean isRowSelectable(JSONObject row);
+    }
+    
     public SelectionManager(DataTable table, boolean selectOnlyOne) {
         attachedTable = table;
         this.selectOnlyOne = selectOnlyOne;
     }
     
+    public void setSelectableRowFilter(SelectableRowFilter filter) {
+        selectableRowFilter = filter;
+    }
+    
     public void refreshSelection() {
         for (int i = 0; i < attachedTable.getRowCount(); i++) {
-            if (selectedObjects.contains(attachedTable.getRow(i)))
+            JSONObject row = attachedTable.getRow(i);
+            if (!isSelectable(row)) {
+                continue;
+            }
+            if (selectedObjects.contains(row)) { 
                 attachedTable.highlightRow(i);
-            else
+            } else {
                 attachedTable.unhighlightRow(i);
+            }
         }
         attachedTable.refreshWidgets();
     }
     
+    private boolean isSelectable(JSONObject row) {
+        if (selectableRowFilter != null) {
+            return selectableRowFilter.isRowSelectable(row);
+        }
+        return true;
+    }
+
     public void selectObject(JSONObject object) {
         selectObjects(Utils.wrapObjectWithList(object));
     }
@@ -75,13 +95,18 @@
                                       boolean add) {
         List<JSONObject> actuallyUsed = new ArrayList<JSONObject>();
         for (JSONObject object : objects) {
+            if (!isSelectable(object)) {
+                continue;
+            }
             boolean used = false;
-            if (add)
+            if (add) {
                 used = selectedObjects.add(object);
-            else
+            } else {
                 used = selectedObjects.remove(object);
-            if (used)
+            }
+            if (used) {
                 actuallyUsed.add(object);
+            }
         }
         notifyListeners(actuallyUsed, add);
     }
@@ -134,12 +159,6 @@
         listeners.remove(listener);
     }
     
-    protected void notifyListeners(JSONObject object, boolean add) {
-        List<JSONObject> objectList = new ArrayList<JSONObject>();
-        objectList.add(object);
-        notifyListeners(objectList, add);
-    }
-    
     protected void notifyListeners(Collection<JSONObject> objects,
                                    boolean add) {
         refreshSelection();
@@ -151,9 +170,13 @@
         }
     }
 
-    // code for acting as a TableWidgetFactory follows
+    // code for acting as a TableWidgetFactory/TableWidgetClickListener
     
     public Widget createWidget(int row, int cell, JSONObject rowObject) {
+        if (!isSelectable(rowObject)) {
+            return null;
+        }
+
         CheckBox checkBox = new CheckBox();
         if(selectedObjects.contains(rowObject)) {
             checkBox.setChecked(true);
@@ -165,6 +188,8 @@
         toggleSelected(attachedTable.getRow(widget.getRow()));
         refreshSelection();
     }
+    
+    // code for acting as a SelectionPanelListener
 
     public void onSelectAll(boolean visibleOnly) {
         if (visibleOnly) {
diff --git a/frontend/client/src/autotest/common/table/SimpleFilter.java b/frontend/client/src/autotest/common/table/SimpleFilter.java
index 8738176..aaafde1 100644
--- a/frontend/client/src/autotest/common/table/SimpleFilter.java
+++ b/frontend/client/src/autotest/common/table/SimpleFilter.java
@@ -22,7 +22,7 @@
     }
     
     public void setAllParameters(JSONObject params) {
-        parameters = new JSONObject();
+        clear();
         updateObject(parameters, params);
     }
 
@@ -41,4 +41,8 @@
         return true;
     }
 
+    public void clear() {
+        parameters = new JSONObject();
+    }
+
 }
diff --git a/frontend/client/src/autotest/common/table/TableDecorator.java b/frontend/client/src/autotest/common/table/TableDecorator.java
index b1ecab8..5a3f938 100644
--- a/frontend/client/src/autotest/common/table/TableDecorator.java
+++ b/frontend/client/src/autotest/common/table/TableDecorator.java
@@ -62,7 +62,7 @@
         addControl(text, filter.getWidget());
     }
     
-    protected void addControl(String text, Widget widget) {
+    public void addControl(String text, Widget widget) {
       int row = numFilters;
       numFilters++;
       filterTable.setText(row, 0, text);
diff --git a/frontend/client/src/autotest/common/ui/TabView.java b/frontend/client/src/autotest/common/ui/TabView.java
index 08e4165..0bc4047 100644
--- a/frontend/client/src/autotest/common/ui/TabView.java
+++ b/frontend/client/src/autotest/common/ui/TabView.java
@@ -1,6 +1,7 @@
 package autotest.common.ui;
 
 import autotest.common.CustomHistory;
+import autotest.common.Utils;
 import autotest.common.CustomHistory.HistoryToken;
 
 import com.google.gwt.http.client.URL;
@@ -100,7 +101,7 @@
     protected void openHistoryToken(HistoryToken historyToken) {
         if (isOpenInNewWindowEvent()) {
             String newUrl = Window.Location.getPath() + "#" + historyToken;
-            Window.open(URL.encode(newUrl), "_blank", "");
+            Utils.openUrlInNewWindow(URL.encode(newUrl));
         } else {
             History.newItem(historyToken.toString());
         }
diff --git a/frontend/client/src/autotest/tko/EmbeddedTkoClient.java b/frontend/client/src/autotest/tko/EmbeddedTkoClient.java
index 3a35f92..0ed0a0b 100644
--- a/frontend/client/src/autotest/tko/EmbeddedTkoClient.java
+++ b/frontend/client/src/autotest/tko/EmbeddedTkoClient.java
@@ -1,6 +1,7 @@
 package autotest.tko;
 
 import autotest.common.JsonRpcProxy;
+import autotest.common.Utils;
 import autotest.common.CustomHistory.HistoryToken;
 import autotest.tko.TableView.TableSwitchListener;
 import autotest.tko.TableView.TableViewConfig;
@@ -10,7 +11,6 @@
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.core.client.GWT.UncaughtExceptionHandler;
 import com.google.gwt.dom.client.Element;
-import com.google.gwt.user.client.Window;
 
 public class EmbeddedTkoClient implements EntryPoint, TableSwitchListener {
     private String autotestServerUrl; 
@@ -84,7 +84,7 @@
 
     public void onSelectTest(int testId) {
         String fullUrl = autotestServerUrl + "/new_tko/#" + getSelectTestHistoryToken(testId);
-        Window.open(fullUrl, "_blank", "");
+        Utils.openUrlInNewWindow(fullUrl);
     }
 
     public void onSwitchToTable(TableViewConfig config) {
diff --git a/frontend/client/src/autotest/tko/ExistingGraphsFrontend.java b/frontend/client/src/autotest/tko/ExistingGraphsFrontend.java
index 6d5824a..2ab11ef 100644
--- a/frontend/client/src/autotest/tko/ExistingGraphsFrontend.java
+++ b/frontend/client/src/autotest/tko/ExistingGraphsFrontend.java
@@ -10,7 +10,6 @@
 import com.google.gwt.json.client.JSONObject;
 import com.google.gwt.json.client.JSONString;
 import com.google.gwt.json.client.JSONValue;
-import com.google.gwt.user.client.Window;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.CheckBox;
 import com.google.gwt.user.client.ui.ClickListener;
@@ -246,7 +245,7 @@
             args.put("benchmark", benchmarkStr);
             args.put("key", getKey(benchmarkStr));
         }
-        Window.open(url + Utils.encodeUrlArguments(args), "_blank", "");
+        Utils.openUrlInNewWindow(url + Utils.encodeUrlArguments(args));
     }
     
     private String getKey(String benchmark) {
diff --git a/frontend/client/src/autotest/tko/TkoUtils.java b/frontend/client/src/autotest/tko/TkoUtils.java
index f68cfd7..25945a1 100644
--- a/frontend/client/src/autotest/tko/TkoUtils.java
+++ b/frontend/client/src/autotest/tko/TkoUtils.java
@@ -12,7 +12,6 @@
 import com.google.gwt.json.client.JSONString;
 import com.google.gwt.json.client.JSONValue;
 import com.google.gwt.user.client.DOM;
-import com.google.gwt.user.client.Window;
 import com.google.gwt.user.client.ui.FlexTable;
 import com.google.gwt.user.client.ui.Widget;
 
@@ -110,7 +109,7 @@
         }
 
         String url = JsonRpcProxy.TKO_BASE_URL + "csv/?" + request.toString();
-        Window.open(url, "_blank", "");
+        Utils.openUrlInNewWindow(url);
     }
     
     static void doCsvRequest(RpcDataSource dataSource) {