Repository: getredash/redash

Issue:
Support for multi select in parameters
When using a Dropdown List type of parameter, the user should be able to define the parameter to take multiple values.

This will require some additional configuration of how to serialize the values. We can probably do away with adding a checkbox for "quote each value".


Discussion:

Resolving patch:
diff --git a/redash/models/parameterized_query.py b/redash/models/parameterized_query.py
--- a/redash/models/parameterized_query.py
+++ b/redash/models/parameterized_query.py
@@ -36,6 +36,21 @@ def dropdown_values(query_id):
     return map(pluck, data["rows"])
 
 
+def join_parameter_list_values(parameters, schema):
+    updated_parameters = {}
+    for (key, value) in parameters.iteritems():
+        if isinstance(value, list):
+            definition = next((definition for definition in schema if definition["name"] == key), {})
+            multi_values_options = definition.get('multiValuesOptions', {})
+            separator = str(multi_values_options.get('separator', ','))
+            prefix = str(multi_values_options.get('prefix', ''))
+            suffix = str(multi_values_options.get('suffix', ''))
+            updated_parameters[key] = separator.join(map(lambda v: prefix + v + suffix, value))
+        else:
+            updated_parameters[key] = value
+    return updated_parameters
+
+
 def _collect_key_names(nodes):
     keys = []
     for node in nodes._parse_tree:
@@ -92,6 +107,12 @@ def _is_date_range(obj):
         return False
 
 
+def _is_value_within_options(value, dropdown_options, allow_list=False):
+    if isinstance(value, list):
+        return allow_list and set(map(unicode, value)).issubset(set(dropdown_options))
+    return unicode(value) in dropdown_options
+
+
 class ParameterizedQuery(object):
     def __init__(self, template, schema=None):
         self.schema = schema or []
@@ -105,7 +126,7 @@ def apply(self, parameters):
             raise InvalidParameterError(invalid_parameter_names)
         else:
             self.parameters.update(parameters)
-            self.query = mustache_render(self.template, self.parameters)
+            self.query = mustache_render(self.template, join_parameter_list_values(parameters, self.schema))
 
         return self
 
@@ -118,11 +139,22 @@ def _valid(self, name, value):
         if not definition:
             return False
 
+        enum_options = definition.get('enumOptions')
+        query_id = definition.get('queryId')
+        allow_multiple_values = isinstance(definition.get('multiValuesOptions'), dict)
+
+        if isinstance(enum_options, basestring):
+            enum_options = enum_options.split('\n')
+
         validators = {
             "text": lambda value: isinstance(value, basestring),
             "number": _is_number,
-            "enum": lambda value: value in definition["enumOptions"],
-            "query": lambda value: unicode(value) in [v["value"] for v in dropdown_values(definition["queryId"])],
+            "enum": lambda value: _is_value_within_options(value,
+                                                           enum_options,
+                                                           allow_multiple_values),
+            "query": lambda value: _is_value_within_options(value,
+                                                            [v["value"] for v in dropdown_values(query_id)],
+                                                            allow_multiple_values),
             "date": _is_date,
             "datetime-local": _is_date,
             "datetime-with-seconds": _is_date,
diff --git a/redash/tasks/queries.py b/redash/tasks/queries.py
--- a/redash/tasks/queries.py
+++ b/redash/tasks/queries.py
@@ -193,7 +193,7 @@ def refresh_queries():
                 if query.options and len(query.options.get('parameters', [])) > 0:
                     query_params = {p['name']: p.get('value')
                                     for p in query.options['parameters']}
-                    query_text = mustache_render(query.query_text, query_params)
+                    query_text = query.parameterized.apply(query_params).query
                 else:
                     query_text = query.query_text
 


Test patch:
diff --git a/tests/models/test_parameterized_query.py b/tests/models/test_parameterized_query.py
--- a/tests/models/test_parameterized_query.py
+++ b/tests/models/test_parameterized_query.py
@@ -119,6 +119,18 @@ def test_raises_on_unlisted_enum_value_parameters(self):
         with pytest.raises(InvalidParameterError):
             query.apply({"bar": "shlomo"})
 
+    def test_raises_on_unlisted_enum_list_value_parameters(self):
+        schema = [{
+            "name": "bar",
+            "type": "enum",
+            "enumOptions": ["baz", "qux"],
+            "multiValuesOptions": {"separator": ",", "prefix": "", "suffix": ""}
+        }]
+        query = ParameterizedQuery("foo", schema)
+
+        with pytest.raises(InvalidParameterError):
+            query.apply({"bar": ["shlomo", "baz"]})
+
     def test_validates_enum_parameters(self):
         schema = [{"name": "bar", "type": "enum", "enumOptions": ["baz", "qux"]}]
         query = ParameterizedQuery("foo {{bar}}", schema)
@@ -127,6 +139,19 @@ def test_validates_enum_parameters(self):
 
         self.assertEquals("foo baz", query.text)
 
+    def test_validates_enum_list_value_parameters(self):
+        schema = [{
+            "name": "bar",
+            "type": "enum",
+            "enumOptions": ["baz", "qux"],
+            "multiValuesOptions": {"separator": ",", "prefix": "'", "suffix": "'"}
+        }]
+        query = ParameterizedQuery("foo {{bar}}", schema)
+
+        query.apply({"bar": ["qux", "baz"]})
+
+        self.assertEquals("foo 'qux','baz'", query.text)
+
     @patch('redash.models.parameterized_query.dropdown_values', return_value=[{"value": "1"}])
     def test_validation_accepts_integer_values_for_dropdowns(self, _):
         schema = [{"name": "bar", "type": "query", "queryId": 1}]