Add almost everything necessary for UI support for including test attributes in table view.

This was almost straightforward.  The one roadblock came with grouping support.  I didn't bother including that for iteration results, but I did try to make it work here.  Grouping is fine; unfortunately, grouping implies drilldown, and drilldown implies arbitrary filtering on test attributes, which requires a bit more sophisticated handling of which attribute fields we're requesting in each query (it's not just the ones we're viewing, but also any fields used in filtering).  This isn't a terribly hard problem to solve, but doing it right will require some design changes, notably separating the fields in the view from the fields used in actual query, which will probably require making HeaderFields immutable to be done properly.  I need to verify that this feature is high-enough priority to justify getting into all that, but I'd like to check in what I've got so far so I don't lose it.

To be clear, this doesn't actually add any user-visible changes yet.

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


git-svn-id: http://test.kernel.org/svn/autotest/trunk@3866 592f7852-d20e-0410-864c-8624ca9c26a4
diff --git a/frontend/client/src/autotest/tko/HeaderField.java b/frontend/client/src/autotest/tko/HeaderField.java
index a1d6aef..be7d64e 100644
--- a/frontend/client/src/autotest/tko/HeaderField.java
+++ b/frontend/client/src/autotest/tko/HeaderField.java
@@ -1,5 +1,6 @@
 package autotest.tko;
 
+import autotest.common.Utils;
 import autotest.common.ui.MultiListSelectPresenter.Item;
 
 import com.google.gwt.json.client.JSONObject;
@@ -29,6 +30,17 @@
     }
 
     /**
+     * A common helper for SQL conditions.
+     */
+    protected String getSimpleSqlCondition(String field, String value) {
+        if (value.equals(Utils.JSON_NULL)) {
+          return field + " is null";
+        } else {
+          return field + " = '" + TkoUtils.escapeSqlValue(value) + "'";
+        }
+    }
+
+    /**
      * Get the SQL WHERE clause to filter on the given value for this header.
      */
     public abstract String getSqlCondition(String value);
diff --git a/frontend/client/src/autotest/tko/IterationResultField.java b/frontend/client/src/autotest/tko/IterationResultField.java
index 9370ba4..e883189 100644
--- a/frontend/client/src/autotest/tko/IterationResultField.java
+++ b/frontend/client/src/autotest/tko/IterationResultField.java
@@ -6,9 +6,8 @@
 import com.google.gwt.json.client.JSONObject;
 import com.google.gwt.json.client.JSONString;
 
-public class IterationResultField extends ParameterizedField {
+public class IterationResultField extends StringParameterizedField {
     public static final String BASE_NAME = "Iteration result";
-    private String attribute;
 
     @Override
     protected ParameterizedField freshInstance() {
@@ -26,16 +25,6 @@
     }
 
     @Override
-    public String getValue() {
-        return attribute;
-    }
-
-    @Override
-    public void setValue(String value) {
-        attribute = value;
-    }
-
-    @Override
     public String getAttributeName() {
         return getValue();
     }
@@ -44,7 +33,7 @@
     public void addQueryParameters(JSONObject parameters) {
         JSONArray iterationKeys = 
             Utils.setDefaultValue(parameters, "result_keys", new JSONArray()).isArray();
-        iterationKeys.set(iterationKeys.size(), new JSONString(attribute));
+        iterationKeys.set(iterationKeys.size(), new JSONString(getValue()));
     }
 
     @Override
diff --git a/frontend/client/src/autotest/tko/ParameterizedField.java b/frontend/client/src/autotest/tko/ParameterizedField.java
index bee87a6..8dc4158 100644
--- a/frontend/client/src/autotest/tko/ParameterizedField.java
+++ b/frontend/client/src/autotest/tko/ParameterizedField.java
@@ -9,6 +9,7 @@
         // add all ParameterizedField subclasses here.  these instances should never escape. 
         new MachineLabelField(),
         new IterationResultField(),
+        new TestAttributeField(),
     };
 
     private int fieldNumber;
diff --git a/frontend/client/src/autotest/tko/SimpleHeaderField.java b/frontend/client/src/autotest/tko/SimpleHeaderField.java
index 2337084..923f4f0 100644
--- a/frontend/client/src/autotest/tko/SimpleHeaderField.java
+++ b/frontend/client/src/autotest/tko/SimpleHeaderField.java
@@ -1,6 +1,5 @@
 package autotest.tko;
 
-import autotest.common.Utils;
 
 class SimpleHeaderField extends HeaderField {
     public SimpleHeaderField(String name, String sqlName) {
@@ -9,10 +8,6 @@
 
     @Override
     public String getSqlCondition(String value) {
-        if (value.equals(Utils.JSON_NULL)) {
-          return getSqlName() + " is null";
-        } else {
-          return getSqlName() + " = '" + TkoUtils.escapeSqlValue(value) + "'";
-        }
+        return getSimpleSqlCondition(getSqlName(), value);
     }
 }
diff --git a/frontend/client/src/autotest/tko/StringParameterizedField.java b/frontend/client/src/autotest/tko/StringParameterizedField.java
new file mode 100644
index 0000000..c7e66eb6
--- /dev/null
+++ b/frontend/client/src/autotest/tko/StringParameterizedField.java
@@ -0,0 +1,15 @@
+package autotest.tko;
+
+public abstract class StringParameterizedField extends ParameterizedField {
+    private String attribute;
+
+    @Override
+    public String getValue() {
+        return attribute;
+    }
+
+    @Override
+    public void setValue(String value) {
+        attribute = value;
+    }
+}
diff --git a/frontend/client/src/autotest/tko/TestAttributeField.java b/frontend/client/src/autotest/tko/TestAttributeField.java
new file mode 100644
index 0000000..2eb8dc4
--- /dev/null
+++ b/frontend/client/src/autotest/tko/TestAttributeField.java
@@ -0,0 +1,44 @@
+package autotest.tko;
+
+import autotest.common.Utils;
+
+import com.google.gwt.json.client.JSONArray;
+import com.google.gwt.json.client.JSONObject;
+import com.google.gwt.json.client.JSONString;
+
+public class TestAttributeField extends StringParameterizedField {
+    public static final String BASE_NAME = "Test attribute";
+
+    @Override
+    protected ParameterizedField freshInstance() {
+        return new TestAttributeField();
+    }
+
+    @Override
+    protected String getBaseName() {
+        return BASE_NAME;
+    }
+
+    @Override
+    public String getBaseSqlName() {
+        return "attribute_";
+    }
+
+    @Override
+    public String getAttributeName() {
+        return "attribute_" + getValue();
+    }
+
+    @Override
+    public void addQueryParameters(JSONObject parameters) {
+        JSONArray testAttributes = 
+            Utils.setDefaultValue(parameters, "test_attributes", new JSONArray()).isArray();
+        testAttributes.set(testAttributes.size(), new JSONString(getValue()));
+    }
+
+    @Override
+    public String getSqlCondition(String value) {
+        return getSimpleSqlCondition(getAttributeName(), value);
+    }
+
+}
diff --git a/new_tko/tko/models.py b/new_tko/tko/models.py
index 8d0cfb1..0884f8f 100644
--- a/new_tko/tko/models.py
+++ b/new_tko/tko/models.py
@@ -15,7 +15,7 @@
         field_names = []
         for field in fields:
             if field in extra_select_fields:
-                field_names.append(field)
+                field_names.append(extra_select_fields[field][0])
             else:
                 field_names.append(self._get_key_unless_is_function(field))
         return field_names
@@ -74,16 +74,21 @@
 
 
     def _get_num_groups_sql(self, query, group_by):
-        group_fields = self._get_field_names(group_by)
+        group_fields = self._get_field_names(group_by, query.query.extra_select)
         query = query.order_by() # this can mess up the query and isn't needed
 
         sql, params = query.query.as_sql()
         from_ = sql[sql.find(' FROM'):]
-        return ('SELECT COUNT(DISTINCT %s) %s' % (','.join(group_fields),
+        return ('SELECT DISTINCT %s %s' % (','.join(group_fields),
                                                   from_),
                 params)
 
 
+    def _cursor_rowcount(self, cursor):
+        """To be stubbed by tests"""
+        return cursor.rowcount
+
+
     def get_num_groups(self, query, group_by):
         """
         Returns the number of distinct groups for the given query grouped by the
@@ -92,7 +97,7 @@
         sql, params = self._get_num_groups_sql(query, group_by)
         cursor = readonly_connection.connection().cursor()
         cursor.execute(sql, params)
-        return cursor.fetchone()[0]
+        return self._cursor_rowcount(cursor)
 
 
 class Machine(dbmodels.Model):
diff --git a/new_tko/tko/rpc_interface.py b/new_tko/tko/rpc_interface.py
index 9976342..52ccb43 100644
--- a/new_tko/tko/rpc_interface.py
+++ b/new_tko/tko/rpc_interface.py
@@ -73,7 +73,8 @@
     """
     Gets the count of unique groups with the given grouping fields.
     """
-    query = models.TestView.query_objects(filter_data)
+    query = models.TestView.objects.get_query_set_with_joins(filter_data)
+    query = models.TestView.query_objects(filter_data, initial_query=query)
     return models.TestView.objects.get_num_groups(query, group_by)
 
 
diff --git a/new_tko/tko/rpc_interface_unittest.py b/new_tko/tko/rpc_interface_unittest.py
index 0105f6c..46c5fa0 100755
--- a/new_tko/tko/rpc_interface_unittest.py
+++ b/new_tko/tko/rpc_interface_unittest.py
@@ -92,12 +92,18 @@
         self._god = mock.mock_god()
         self._god.stub_with(models.TempManager, '_get_column_names',
                             self._get_column_names_for_sqlite3)
+        self._god.stub_with(models.TempManager, '_cursor_rowcount',
+                            self._cursor_rowcount_for_sqlite3)
         setup_test_environment.set_up()
         fix_iteration_tables()
         setup_test_view()
         self._create_initial_data()
 
 
+    def _cursor_rowcount_for_sqlite3(self, cursor):
+        return len(cursor.fetchall())
+
+
     def tearDown(self):
         setup_test_environment.tear_down()
         self._god.unstub_all()
@@ -418,10 +424,14 @@
 
 
     def test_grouping_with_test_attributes(self):
+        num_groups = rpc_interface.get_num_groups(['attribute_myattr'],
+                                                test_attributes=['myattr'])
+        self.assertEquals(num_groups, 2)
+
         counts = rpc_interface.get_group_counts(['attribute_myattr'],
                                                 test_attributes=['myattr'])
         groups = counts['groups']
-        self.assertEquals(len(groups), 2)
+        self.assertEquals(len(groups), num_groups)
         self.assertEquals(groups[0]['attribute_myattr'], None)
         self.assertEquals(groups[0]['group_count'], 2)
         self.assertEquals(groups[1]['attribute_myattr'], 'myval')