-add feature to filter on test attributes in TKO
  -new server arguments "include_attributes_where" and "exclude_attributes_where" for filtering on test attributes
  -refactor joining code in TKO models.py to support test attributes joining
  -add new UI to CommonPanel.java to filter on test attributes. some of the UI code was written in a general way so that in the future it could be merged with some of the graphing UI code.
-modified TestSets and code that uses them to fix two bugs - first, TestSets didn't contain all the relevant filtering information (only the SQL clause), and second, the SQL clause would build up incorrectly during drilldown



git-svn-id: http://test.kernel.org/svn/autotest/trunk@2177 592f7852-d20e-0410-864c-8624ca9c26a4
diff --git a/frontend/client/src/autotest/common/Utils.java b/frontend/client/src/autotest/common/Utils.java
index 1997635..7583f1c 100644
--- a/frontend/client/src/autotest/common/Utils.java
+++ b/frontend/client/src/autotest/common/Utils.java
@@ -103,14 +103,19 @@
     }
     
     public static <T> String joinStrings(String joiner, List<T> objects) {
-        if (objects.size() == 0) {
-            return "";
-        }
-        
-        StringBuilder result = new StringBuilder(objects.get(0).toString());
-        for (int i = 1; i < objects.size(); i++) {
-            result.append(joiner);
-            result.append(objects.get(i).toString());
+        StringBuilder result = new StringBuilder();
+        boolean first = true;
+        for (T object : objects) {
+            String piece = object.toString();
+            if (piece.equals("")) {
+                continue;
+            }
+            if (first) {
+                first = false;
+            } else {
+                result.append(joiner);
+            }
+            result.append(piece);
         }
         return result.toString();
     }
diff --git a/frontend/client/src/autotest/tko/CommonPanel.java b/frontend/client/src/autotest/tko/CommonPanel.java
index ca7b42f..b1bfc34 100644
--- a/frontend/client/src/autotest/tko/CommonPanel.java
+++ b/frontend/client/src/autotest/tko/CommonPanel.java
@@ -4,6 +4,7 @@
 import autotest.common.ui.ElementWidget;
 import autotest.common.ui.SimpleHyperlink;
 import autotest.tko.TkoUtils.FieldInfo;
+import autotest.tko.WidgetList.ListWidgetFactory;
 
 import com.google.gwt.json.client.JSONObject;
 import com.google.gwt.json.client.JSONString;
@@ -11,14 +12,20 @@
 import com.google.gwt.user.client.ui.ClickListener;
 import com.google.gwt.user.client.ui.Composite;
 import com.google.gwt.user.client.ui.FlexTable;
+import com.google.gwt.user.client.ui.HTML;
+import com.google.gwt.user.client.ui.HorizontalPanel;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.ListBox;
 import com.google.gwt.user.client.ui.Panel;
 import com.google.gwt.user.client.ui.PopupPanel;
 import com.google.gwt.user.client.ui.RootPanel;
 import com.google.gwt.user.client.ui.TextArea;
+import com.google.gwt.user.client.ui.TextBox;
 import com.google.gwt.user.client.ui.VerticalPanel;
 import com.google.gwt.user.client.ui.Widget;
 import com.google.gwt.user.client.ui.PopupPanel.PositionCallback;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
@@ -26,21 +33,80 @@
 import java.util.Set;
 
 class CommonPanel extends Composite implements ClickListener, PositionCallback {
+    private static final String WIKI_URL = "http://autotest.kernel.org/wiki/TkoHowTo";
     private static final String SHOW_QUICK_REFERENCE = "Show quick reference";
     private static final String HIDE_QUICK_REFERENCE = "Hide quick reference";
     private static final String SHOW_CONTROLS = "Show controls";
     private static final String HIDE_CONTROLS = "Hide controls";
+    private static final String INCLUDE_ATTRIBUTES_TABLE = "test_attributes_include";
+    private static final String EXCLUDE_ATTRIBUTES_TABLE = "test_attributes_exclude";
     private static CommonPanel theInstance = new CommonPanel();
     
+    private class AttributeFilter extends Composite implements ClickListener {
+        private ListBox includeOrExclude = new ListBox();
+        private TextBox attributeWhere = new TextBox(), valueWhere = new TextBox();
+        
+        public AttributeFilter() {
+            includeOrExclude.addItem("Include");
+            includeOrExclude.addItem("Exclude");
+            
+            Panel panel = new HorizontalPanel();
+            panel.add(includeOrExclude);
+            panel.add(new Label("tests with attribute"));
+            panel.add(attributeWhere);
+            panel.add(new Label("and value"));
+            panel.add(valueWhere);
+            
+            SimpleHyperlink deleteLink = new SimpleHyperlink("[X]");
+            deleteLink.addClickListener(this);
+            panel.add(deleteLink);
+            
+            initWidget(panel);
+        }
+
+        public void onClick(Widget sender) {
+            attributeFilterList.deleteWidget(this);
+        }
+        
+        public boolean isInclude() {
+            return includeOrExclude.getSelectedIndex() == 0;
+        }
+        
+        public String getFilterString() {
+            String tableName;
+            if (isInclude()) {
+                tableName = INCLUDE_ATTRIBUTES_TABLE;
+            } else {
+                tableName = EXCLUDE_ATTRIBUTES_TABLE;
+            }
+            
+            return "(" + tableName + ".attribute " + attributeWhere.getText() + " AND " +
+                   tableName + ".value " + valueWhere.getText() + ")";
+        }
+        
+        public void addToHistory(Map<String, String> args, String prefix) {
+            args.put(prefix + "_include", Boolean.toString(isInclude()));
+            args.put(prefix + "_attribute", attributeWhere.getText());
+            args.put(prefix + "_value", valueWhere.getText());
+        }
+    }
+    
+    private class AttributeFilterFactory implements ListWidgetFactory<AttributeFilter> {
+        public AttributeFilter getNewWidget() {
+            return new AttributeFilter();
+        }
+    }
+    
     private TextArea customSqlBox = new TextArea();
     private CheckBox showInvalid = new CheckBox("Show invalidated tests");
     private SimpleHyperlink quickReferenceLink = new SimpleHyperlink(SHOW_QUICK_REFERENCE);
     private PopupPanel quickReferencePopup;
     private SimpleHyperlink showHideControlsLink = new SimpleHyperlink(HIDE_CONTROLS);
     private Panel allControlsPanel = RootPanel.get("common_all_controls");
-    private String currentCondition = "";
-    private boolean currentShowInvalid = false;
+    private boolean savedShowInvalid = false;
+    private JSONObject savedCondition = new JSONObject();
     private Set<CommonPanelListener> listeners = new HashSet<CommonPanelListener>();
+    private WidgetList<AttributeFilter> attributeFilterList;
     
     public static interface CommonPanelListener {
         public void onSetControlsVisible(boolean visible);
@@ -56,8 +122,20 @@
         quickReferenceLink.addClickListener(this);
         showHideControlsLink.addClickListener(this);
         
+        attributeFilterList = 
+            new WidgetList<AttributeFilter>(new AttributeFilterFactory(), "Add attribute filter");
+        Panel titlePanel = new HorizontalPanel();
+        titlePanel.add(getFieldLabel("Test attributes:"));
+        titlePanel.add(new HTML("&nbsp;<a href=\"" + WIKI_URL + "#attribute_filtering\" " +
+                                "target=\"_blank\">[?]</a>"));
+        Panel attributeFilters = new VerticalPanel();
+        attributeFilters.setStyleName("box");
+        attributeFilters.add(titlePanel);
+        attributeFilters.add(attributeFilterList);
+        
         Panel commonFilterPanel = new VerticalPanel();
         commonFilterPanel.add(customSqlBox);
+        commonFilterPanel.add(attributeFilters);
         commonFilterPanel.add(showInvalid);
         RootPanel.get("common_filters").add(commonFilterPanel);
         RootPanel.get("common_quick_reference").add(quickReferenceLink);
@@ -65,6 +143,12 @@
         generateQuickReferencePopup();
     }
     
+    private Widget getFieldLabel(String string) {
+        Label label = new Label(string);
+        label.setStyleName("field-name");
+        return label;
+    }
+
     public static CommonPanel getPanel() {
         return theInstance;
     }
@@ -84,44 +168,106 @@
         return customSqlBox.getText().trim();
     }
     
-    private void setSqlCondition(String text) {
+    public void setSqlCondition(String text) {
         customSqlBox.setText(text);
         saveSqlCondition();
     }
     
-    public void saveSqlCondition() {
-        currentCondition = getSqlCondition();
-        currentShowInvalid = showInvalid.isChecked();
+    private void saveAttributeFilters() {
+        List<String> include = new ArrayList<String>(), exclude = new ArrayList<String>();
+        for (AttributeFilter filter : attributeFilterList.getWidgets()) {
+            if (filter.isInclude()) {
+                include.add(filter.getFilterString());
+            } else {
+                exclude.add(filter.getFilterString());
+            }
+        }
+        
+        String includeSql = Utils.joinStrings(" OR ", include);
+        String excludeSql = Utils.joinStrings(" OR ", exclude);
+        saveIfNonempty("include_attributes_where", includeSql);
+        saveIfNonempty("exclude_attributes_where", excludeSql);
     }
     
-    public JSONObject getSavedConditionArgs() {
-        JSONObject args = new JSONObject();
-        args.put("extra_where", new JSONString(currentCondition));
-        if (!currentShowInvalid) {
-            List<String> labelsToExclude = Arrays.asList(new String[] {"invalidated"});
-            args.put("exclude_labels", Utils.stringsToJSON(labelsToExclude));
+    public void saveSqlCondition() {
+        savedCondition = new JSONObject();
+        saveIfNonempty("extra_where", getSqlCondition());
+        saveAttributeFilters();
+        
+        savedShowInvalid = showInvalid.isChecked();
+        if (!savedShowInvalid) {
+            List<String> labelsToExclude = 
+                Arrays.asList(new String[] {TestLabelManager.INVALIDATED_LABEL});
+            savedCondition.put("exclude_labels", Utils.stringsToJSON(labelsToExclude));
         }
-        return args;
+    }
+    
+    private void saveIfNonempty(String key, String value) {
+        if (value.equals("")) {
+            return;
+        }
+        savedCondition.put(key, new JSONString(value));
     }
 
-    public void setCondition(TestSet tests) {
-        setSqlCondition(tests.getCondition());
+    public JSONObject getSavedConditionArgs() {
+        return Utils.copyJSONObject(savedCondition);
+    }
+
+    public void refineCondition(TestSet tests) {
+        String sqlCondition = TkoUtils.getSqlCondition(savedCondition);
+        String newCondition = tests.getPartialSqlCondition();
+        setSqlCondition(TkoUtils.joinWithParens(" AND ", sqlCondition, newCondition));
+    }
+    
+    private String getListKey(String base, int index) {
+        return base + "_" + Integer.toString(index);
+    }
+    
+    public AttributeFilter attributeFilterFromHistory(Map<String, String> args, String prefix) {
+        String includeKey = prefix + "_include";
+        if (!args.containsKey(includeKey)) {
+            return null;
+        }
+        
+        AttributeFilter filter = new AttributeFilter();
+        boolean include = Boolean.valueOf(args.get(includeKey));
+        filter.includeOrExclude.setSelectedIndex(include ? 0 : 1);
+        filter.attributeWhere.setText(args.get(prefix + "_attribute"));
+        filter.valueWhere.setText(args.get(prefix + "_value"));
+        return filter;
     }
 
     public void handleHistoryArguments(Map<String, String> arguments) {
         setSqlCondition(arguments.get("condition"));
-        currentShowInvalid = Boolean.valueOf(arguments.get("show_invalid"));
-        showInvalid.setChecked(currentShowInvalid);
+        savedShowInvalid = Boolean.valueOf(arguments.get("show_invalid"));
+        showInvalid.setChecked(savedShowInvalid);
+        
+        attributeFilterList.clear();
+        for (int index = 0; ; index++) {
+            AttributeFilter filter = attributeFilterFromHistory(arguments,
+                                                                getListKey("attribute", index));
+            if (filter == null) {
+                break;
+            }
+            attributeFilterList.addWidget(filter);
+        }
     }
     
     public void addHistoryArguments(Map<String, String> arguments) {
-        arguments.put("condition", currentCondition);
-        arguments.put("show_invalid", Boolean.toString(currentShowInvalid));
+        if (savedCondition.containsKey("extra_where")) {
+            arguments.put("condition", savedCondition.get("extra_where").isString().stringValue());
+        }
+        arguments.put("show_invalid", Boolean.toString(savedShowInvalid));
+        int index = 0;
+        for (AttributeFilter filter : attributeFilterList.getWidgets()) {
+            filter.addToHistory(arguments, getListKey("attribute", index));
+            index++;
+        }
     }
 
     public void fillDefaultHistoryValues(Map<String, String> arguments) {
         Utils.setDefaultValue(arguments, "condition", "");
-        Utils.setDefaultValue(arguments, "show_invalid", Boolean.toString(currentShowInvalid));
+        Utils.setDefaultValue(arguments, "show_invalid", Boolean.toString(savedShowInvalid));
     }
 
     public void onClick(Widget sender) {
diff --git a/frontend/client/src/autotest/tko/CompositeTestSet.java b/frontend/client/src/autotest/tko/CompositeTestSet.java
index 60abf65..4b80ecf 100644
--- a/frontend/client/src/autotest/tko/CompositeTestSet.java
+++ b/frontend/client/src/autotest/tko/CompositeTestSet.java
@@ -2,24 +2,35 @@
 
 import autotest.common.Utils;
 
+import com.google.gwt.json.client.JSONObject;
+
 import java.util.ArrayList;
 import java.util.List;
 
-class CompositeTestSet implements TestSet {
+class CompositeTestSet extends TestSet {
     private List<TestSet> testSets = new ArrayList<TestSet>();
     
     public void add(TestSet tests) {
         testSets.add(tests);
     }
+    
+    @Override
+    public JSONObject getInitialCondition() {
+        // we assume the initial condition is the same for all tests
+        assert !testSets.isEmpty();
+        return testSets.get(0).getInitialCondition();
+    }
 
-    public String getCondition() {
+    @Override
+    public String getPartialSqlCondition() {
         List<String> conditionParts = new ArrayList<String>();
         for(TestSet testSet : testSets) {
-            conditionParts.add("(" + testSet.getCondition() + ")");
+            conditionParts.add("(" + testSet.getPartialSqlCondition() + ")");
         }
         return Utils.joinStrings(" OR ", conditionParts);
     }
 
+    @Override
     public boolean isSingleTest() {
         return testSets.size() == 1 && testSets.get(0).isSingleTest();
     }
diff --git a/frontend/client/src/autotest/tko/ConditionTestSet.java b/frontend/client/src/autotest/tko/ConditionTestSet.java
index 4176d67..74df7a7 100644
--- a/frontend/client/src/autotest/tko/ConditionTestSet.java
+++ b/frontend/client/src/autotest/tko/ConditionTestSet.java
@@ -9,7 +9,7 @@
 import java.util.List;
 import java.util.Map;
 
-class ConditionTestSet implements TestSet {
+class ConditionTestSet extends TestSet {
     private Map<String,String> fields = new HashMap<String,String>();
     private boolean isSingleTest;
     private JSONObject initialCondition = new JSONObject();
@@ -34,13 +34,14 @@
         }
     }
     
-    public String getCondition() {
+    @Override
+    public JSONObject getInitialCondition() {
+        return Utils.copyJSONObject(initialCondition);
+    }
+
+    @Override
+    public String getPartialSqlCondition() {
         ArrayList<String> parts = new ArrayList<String>();
-        String sqlCondition = TkoUtils.getSqlCondition(initialCondition);
-        if (!sqlCondition.trim().equals("")) {
-            parts.add(sqlCondition);
-        }
-        
         for (Map.Entry<String, String> entry : fields.entrySet()) {
             String query = entry.getKey();  
             String value = entry.getValue();
@@ -60,6 +61,7 @@
         return value.replace("'", "\\'");
     }
 
+    @Override
     public boolean isSingleTest() {
         return isSingleTest;
     }
diff --git a/frontend/client/src/autotest/tko/MachineQualHistogramFrontend.java b/frontend/client/src/autotest/tko/MachineQualHistogramFrontend.java
index bf74a09..509bd40 100644
--- a/frontend/client/src/autotest/tko/MachineQualHistogramFrontend.java
+++ b/frontend/client/src/autotest/tko/MachineQualHistogramFrontend.java
@@ -119,15 +119,7 @@
     
     @SuppressWarnings("unused")
     private void showDrilldown(final String filterString) {
-        CommonPanel.getPanel().setCondition(new TestSet() {
-            public String getCondition() {
-                return filterString;
-            }
-            
-            public boolean isSingleTest() {
-                return false;
-            }
-        });
+        CommonPanel.getPanel().setSqlCondition(filterString);
         listener.onSwitchToTable(TableViewConfig.PASS_RATE);
     }
     
diff --git a/frontend/client/src/autotest/tko/SpreadsheetView.java b/frontend/client/src/autotest/tko/SpreadsheetView.java
index 1232892..a65b148 100644
--- a/frontend/client/src/autotest/tko/SpreadsheetView.java
+++ b/frontend/client/src/autotest/tko/SpreadsheetView.java
@@ -338,7 +338,7 @@
     }
 
     private void doDrilldown(TestSet tests, String newRowField, String newColumnField) {
-        commonPanel.setCondition(tests);
+        commonPanel.refineCondition(tests);
         currentRowFields = HeaderImpl.fromBaseType(Utils.wrapObjectWithList(newRowField));
         currentColumnFields = HeaderImpl.fromBaseType(Utils.wrapObjectWithList(newColumnField));
         updateWidgets();
@@ -453,7 +453,7 @@
     }
 
     private void switchToTable(final TestSet tests, boolean isTriageView) {
-        commonPanel.setCondition(tests);
+        commonPanel.refineCondition(tests);
         TableViewConfig config;
         if (isTriageView) {
             config = TableViewConfig.TRIAGE;
diff --git a/frontend/client/src/autotest/tko/TableView.java b/frontend/client/src/autotest/tko/TableView.java
index e5d084a..7c2a84b 100644
--- a/frontend/client/src/autotest/tko/TableView.java
+++ b/frontend/client/src/autotest/tko/TableView.java
@@ -318,7 +318,7 @@
     }
 
     private void doDrilldown(TestSet testSet) {
-        commonPanel.setCondition(testSet);
+        commonPanel.refineCondition(testSet);
         uncheckBothCheckboxes();
         updateCheckboxes();
         selectColumns(DEFAULT_COLUMNS);
diff --git a/frontend/client/src/autotest/tko/TestContextMenu.java b/frontend/client/src/autotest/tko/TestContextMenu.java
index 43d0632..6d107e1 100644
--- a/frontend/client/src/autotest/tko/TestContextMenu.java
+++ b/frontend/client/src/autotest/tko/TestContextMenu.java
@@ -2,6 +2,7 @@
 
 import autotest.common.ui.ContextMenu;
 
+import com.google.gwt.json.client.JSONObject;
 import com.google.gwt.user.client.Command;
 
 public class TestContextMenu extends ContextMenu {
@@ -28,7 +29,7 @@
     }
     
     public void addLabelItems() {
-        final String condition = tests.getCondition();
+        final JSONObject condition = tests.getCondition();
         addItem("Invalidate tests", new Command() {
             public void execute() {
                 labelManager.handleInvalidate(condition);
diff --git a/frontend/client/src/autotest/tko/TestLabelManager.java b/frontend/client/src/autotest/tko/TestLabelManager.java
index 48e1418..69e35fa 100644
--- a/frontend/client/src/autotest/tko/TestLabelManager.java
+++ b/frontend/client/src/autotest/tko/TestLabelManager.java
@@ -24,6 +24,7 @@
 import com.google.gwt.user.client.ui.Widget;
 
 public class TestLabelManager implements ClickListener {
+    public static final String INVALIDATED_LABEL = "invalidated";
     private static final String ADD_TEXT = "Add label";
     private static final String REMOVE_TEXT = "Remove label";
     private static final int STACK_SELECT = 0, STACK_CREATE = 1;
@@ -41,7 +42,7 @@
     private StackPanel stack = new StackPanel();
     private Button submitButton = new Button(), cancelButton = new Button("Cancel");
     
-    private String currentTestCondition;
+    private JSONObject currentTestCondition;
     
     
     private TestLabelManager() {
@@ -93,7 +94,7 @@
         cancelCreateLink.setVisible(visible);
     }
     
-    public void handleAddLabels(String testCondition) {
+    public void handleAddLabels(JSONObject testCondition) {
         currentTestCondition = testCondition;
         newLabelName.setText("");
         
@@ -110,12 +111,10 @@
         showDialog(ADD_TEXT);
     }
     
-    public void handleRemoveLabels(String testCondition) {
+    public void handleRemoveLabels(JSONObject testCondition) {
         currentTestCondition = testCondition;
         
-        JSONObject args = new JSONObject();
-        args.put("extra_where", new JSONString(currentTestCondition));
-        rpcProxy.rpcCall("get_test_labels_for_tests", args, new JsonRpcCallback() {
+        rpcProxy.rpcCall("get_test_labels_for_tests", currentTestCondition, new JsonRpcCallback() {
             @Override
             public void onSuccess(JSONValue result) {
                 String[] labels = Utils.JSONObjectsToStrings(result.isArray(), "name");
@@ -193,9 +192,8 @@
             rpcMethod = "test_label_remove_tests";
         }
         
-        JSONObject args = new JSONObject();
+        JSONObject args = Utils.copyJSONObject(currentTestCondition);
         args.put("label_id", new JSONString(label));
-        args.put("extra_where", new JSONString(currentTestCondition));
         rpcProxy.rpcCall(rpcMethod, args, new JsonRpcCallback() {
             @Override
             public void onSuccess(JSONValue result) {
@@ -213,8 +211,8 @@
         });
     }
 
-    public void handleInvalidate(String condition) {
+    public void handleInvalidate(JSONObject condition) {
         currentTestCondition = condition;
-        addOrRemoveLabel("invalidated", true);
+        addOrRemoveLabel(INVALIDATED_LABEL, true);
     }
 }
diff --git a/frontend/client/src/autotest/tko/TestSet.java b/frontend/client/src/autotest/tko/TestSet.java
index 3e450f0..71436c5 100644
--- a/frontend/client/src/autotest/tko/TestSet.java
+++ b/frontend/client/src/autotest/tko/TestSet.java
@@ -1,6 +1,24 @@
 package autotest.tko;
 
-interface TestSet {
-    public String getCondition();
-    public boolean isSingleTest();
+import com.google.gwt.json.client.JSONObject;
+import com.google.gwt.json.client.JSONString;
+
+abstract class TestSet {
+    /**
+     * Get the full condition args for this test set.
+     */
+    public abstract JSONObject getInitialCondition();
+    /**
+     * Get the SQL condition for this test set within the global set.
+     */
+    public abstract String getPartialSqlCondition();
+    public abstract boolean isSingleTest();
+    
+    public JSONObject getCondition() {
+        JSONObject condition = getInitialCondition();
+        String sqlCondition = TkoUtils.getSqlCondition(condition); 
+        sqlCondition = TkoUtils.joinWithParens(" AND ", sqlCondition, getPartialSqlCondition());
+        condition.put("extra_where", new JSONString(sqlCondition));
+        return condition;
+    }
 }
diff --git a/frontend/client/src/autotest/tko/TkoClient.java b/frontend/client/src/autotest/tko/TkoClient.java
index c3a44dc..b6545a8 100644
--- a/frontend/client/src/autotest/tko/TkoClient.java
+++ b/frontend/client/src/autotest/tko/TkoClient.java
@@ -52,9 +52,9 @@
         
         final RootPanel tabsRoot = RootPanel.get("tabs");
         tabsRoot.add(mainTabPanel);
+        commonPanel.initialize();
         CustomHistory.processInitialToken();
         mainTabPanel.initialize();
-        commonPanel.initialize();
         tabsRoot.removeStyleName("hidden");
     }
     
diff --git a/frontend/client/src/autotest/tko/TkoUtils.java b/frontend/client/src/autotest/tko/TkoUtils.java
index 92651de..05142a7 100644
--- a/frontend/client/src/autotest/tko/TkoUtils.java
+++ b/frontend/client/src/autotest/tko/TkoUtils.java
@@ -4,6 +4,7 @@
 import autotest.common.JsonRpcCallback;
 import autotest.common.JsonRpcProxy;
 import autotest.common.StaticDataRepository;
+import autotest.common.Utils;
 
 import com.google.gwt.dom.client.Element;
 import com.google.gwt.json.client.JSONArray;
@@ -13,6 +14,7 @@
 import com.google.gwt.user.client.DOM;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 
 public class TkoUtils {
@@ -47,8 +49,7 @@
     }
 
     protected static void getTestId(TestSet test, final TestSelectionListener listener) {
-        rpcProxy.rpcCall("get_test_views", getConditionParams(test.getCondition()), 
-                         new JsonRpcCallback() {
+        rpcProxy.rpcCall("get_test_views", test.getCondition(), new JsonRpcCallback() {
             @Override
             public void onSuccess(JSONValue result) {
                 // just take the first result (there could be more than one due to
@@ -80,4 +81,11 @@
         }
         return condition.isString().stringValue();
     }
+    
+    static String joinWithParens(String joiner, String first, String second) {
+        if (!first.equals("")) {
+            first = "(" + first + ")";
+        }
+        return Utils.joinStrings(" AND ", Arrays.asList(new String[] {first, second}));
+    }
 }
diff --git a/new_tko/tko/models.py b/new_tko/tko/models.py
index b25a24e..70802d3 100644
--- a/new_tko/tko/models.py
+++ b/new_tko/tko/models.py
@@ -247,39 +247,62 @@
         return query.extra(select=extra_select)
 
 
-    def _add_label_joins(self, query_set, suffix = '', join_condition='',
-                         exclude=False):
+    def _add_join(self, query_set, join_table, join_condition='',
+                  join_key='test_idx', suffix='', exclude=False,
+                  force_left_join=False):
         table_name = self.model._meta.db_table
-        first_join_alias = 'test_labels_tests' + suffix
-        first_join_condition = '%s.test_id = %s.test_idx' % (first_join_alias,
-                                                             table_name)
+        join_alias = join_table + suffix
+        full_join_key = join_alias + '.' + join_key
+        full_join_condition = '%s = %s.test_idx' % (full_join_key, table_name)
         if join_condition:
-            first_join_condition += ' AND ' + join_condition
+            full_join_condition += ' AND (' + join_condition + ')'
+        if exclude or force_left_join:
+            join_type = 'LEFT JOIN'
+        else:
+            join_type = 'INNER JOIN'
+
         filter_object = self._CustomSqlQ()
-        filter_object.add_join('test_labels_tests',
-                               first_join_condition,
-                               'LEFT JOIN',
-                               alias=first_join_alias)
+        filter_object.add_join(join_table,
+                               full_join_condition,
+                               join_type,
+                               alias=join_alias)
+        if exclude:
+            filter_object.add_where(full_join_key + ' IS NULL')
+        return query_set.filter(filter_object).distinct()
+
+
+    def _add_label_joins(self, query_set, suffix=''):
+        query_set = self._add_join(query_set, 'test_labels_tests',
+                                   join_key='test_id', suffix=suffix,
+                                   force_left_join=True)
 
         second_join_alias = 'test_labels' + suffix
         second_join_condition = ('%s.id = %s.testlabel_id' %
-                                 (second_join_alias, first_join_alias))
+                                 (second_join_alias,
+                                  'test_labels_tests' + suffix))
+        filter_object = self._CustomSqlQ()
         filter_object.add_join('test_labels',
                                second_join_condition,
                                'LEFT JOIN',
                                alias=second_join_alias)
+        return query_set.filter(filter_object)
 
-        if exclude:
-            filter_object.add_where(first_join_alias + '.testlabel_id IS NULL')
-        return query_set.filter(filter_object).distinct()
+
+    def _add_attribute_join(self, query_set, suffix='', join_condition='',
+                            exclude=False):
+        return self._add_join(query_set, 'test_attributes',
+                              join_condition=join_condition,
+                              suffix=suffix, exclude=exclude)
 
 
     def _get_label_ids_from_names(self, label_names):
+        if not label_names:
+            return []
         query = TestLabel.objects.filter(name__in=label_names).values('id')
         return [label['id'] for label in query]
 
 
-    def get_query_set_with_labels(self, filter_data):
+    def get_query_set_with_joins(self, filter_data):
         exclude_labels = filter_data.pop('exclude_labels', [])
         query_set = self.get_query_set()
         joined = False
@@ -288,15 +311,33 @@
             query_set = self._add_label_joins(query_set)
             joined = True
 
-        if exclude_labels:
-            label_ids = self._get_label_ids_from_names(exclude_labels)
-            if label_ids:
-                condition = ('test_labels_tests_exclude.testlabel_id IN (%s)' %
-                             ','.join(str(label_id) for label_id in label_ids))
-                query_set = self._add_label_joins(query_set, suffix='_exclude',
-                                                  join_condition=condition,
-                                                  exclude=True)
-                joined = True
+        exclude_label_ids = self._get_label_ids_from_names(exclude_labels)
+        if exclude_label_ids:
+            condition = ('test_labels_tests_exclude.testlabel_id IN (%s)' %
+                         ','.join(str(label_id)
+                                  for label_id in exclude_label_ids))
+            query_set = self._add_join(query_set, 'test_labels_tests',
+                                       join_key='test_id',
+                                       suffix='_exclude',
+                                       join_condition=condition,
+                                       exclude=True)
+            joined = True
+
+        include_attributes_where = filter_data.pop('include_attributes_where',
+                                                   '')
+        exclude_attributes_where = filter_data.pop('exclude_attributes_where',
+                                                   '')
+        if include_attributes_where:
+            query_set = self._add_attribute_join(
+                query_set, suffix='_include',
+                join_condition=include_attributes_where)
+            joined = True
+        if exclude_attributes_where:
+            query_set = self._add_attribute_join(
+                query_set, suffix='_exclude',
+                join_condition=exclude_attributes_where,
+                exclude=True)
+            joined = True
 
         if not joined:
             filter_data['no_distinct'] = True
@@ -384,7 +425,7 @@
     @classmethod
     def query_objects(cls, filter_data, initial_query=None):
         if initial_query is None:
-            initial_query = cls.objects.get_query_set_with_labels(filter_data)
+            initial_query = cls.objects.get_query_set_with_joins(filter_data)
         return super(TestView, cls).query_objects(filter_data,
                                                   initial_query=initial_query)