Repository: voxpupuli/puppetboard
Issue: Numerical facts don't get filtered properly

I noticed an issue with trying to filter numeric facts. For example I need to track version upgrades for our 600 Windows 10 PCs so I use the /fact/windows_release_id fact. If I want to view version 1909 for example, I would put /fact/windows_release_id/1909. However when I do this, it shows me all Windows 10's regardless of version number. I instead have to use the search box and type in "1909" to get the filter I need.

Changed files:
test/test_utils.py, puppetboard/utils.py, puppetboard/app.py

diff --git a/puppetboard/app.py b/puppetboard/app.py
index f243d2a..a9ed61e 100644
--- a/puppetboard/app.py
+++ b/puppetboard/app.py
@@ -4,7 +4,6 @@ from __future__ import unicode_literals
 from urllib.parse import unquote, unquote_plus, quote_plus
 from datetime import datetime, timedelta
 from itertools import tee
-from distutils.util import strtobool
 from flask import (
     render_template, abort, url_for,
     Response, stream_with_context, request, session, jsonify
@@ -21,7 +20,7 @@ from pypuppetdb.QueryBuilder import (ExtractOperator, AndOperator,
 
 from puppetboard.forms import ENABLED_QUERY_ENDPOINTS, QueryForm
 from puppetboard.utils import (get_or_abort, yield_or_stop,
-                               get_db_version, is_bool)
+                               get_db_version, parse_python)
 from puppetboard.dailychart import get_daily_reports_chart
 
 import commonmark
@@ -748,30 +747,22 @@ def fact_ajax(env, node, fact, value):
     check_env(env, envs)
 
     render_graph = False
-    if fact in graph_facts and not value and not node:
+    if fact in graph_facts and value is None and node is None:
         render_graph = True
 
     query = AndOperator()
-    if node:
+    if node is not None:
         query.add(EqualsOperator("certname", node))
 
     if env != '*':
         query.add(EqualsOperator("environment", env))
 
-    if len(query.operations) == 0:
-        query = None
-
-    # Generator needs to be converted (graph / total)
-    try:
-        value = int(value)
-    except ValueError:
-        if value is not None and query is not None:
-            if is_bool(value):
-                query.add(EqualsOperator('value', bool(strtobool(value))))
-            else:
-                query.add(EqualsOperator('value', unquote_plus(value)))
-    except TypeError:
-        pass
+    if value is not None:
+        value = unquote_plus(value)
+        # interpret the value as a proper type...
+        value = parse_python(value)
+        # ...to know if it should be quoted or not in the result query
+        query.add(EqualsOperator('value', value))
 
     facts = [f for f in get_or_abort(
         puppetdb.facts,
@@ -789,20 +780,21 @@ def fact_ajax(env, node, fact, value):
 
     for fact_h in facts:
         line = []
-        if not fact:
+        if fact is None:
             line.append(fact_h.name)
-        if not node:
+        if node is None:
             line.append('<a href="{0}">{1}</a>'.format(
                 url_for('node', env=env, node_name=fact_h.node),
                 fact_h.node))
-        if not value:
-            fact_value = fact_h.value
-            if isinstance(fact_value, str):
-                fact_value = quote_plus(fact_h.value)
+        if value is None:
+            if isinstance(fact_h.value, str):
+                value_for_url = quote_plus(fact_h.value)
+            else:
+                value_for_url = fact_h.value
 
             line.append('<a href="{0}">{1}</a>'.format(
                 url_for(
-                    'fact', env=env, fact=fact_h.name, value=fact_value),
+                    'fact', env=env, fact=fact_h.name, value=value_for_url),
                 fact_h.value))
 
         json['data'].append(line)
diff --git a/puppetboard/utils.py b/puppetboard/utils.py
index b684551..b3b3cd8 100644
--- a/puppetboard/utils.py
+++ b/puppetboard/utils.py
@@ -1,7 +1,7 @@
+import ast
 import json
 import logging
 import os.path
-from distutils.util import strtobool
 
 from flask import abort, request, url_for
 from jinja2.utils import contextfunction
@@ -54,6 +54,18 @@ def get_db_version(puppetdb):
     return ver
 
 
+def parse_python(value: str):
+    """
+    :param value: any string, number, bool, list or a dict
+                  casted to a string (f.e. "{'up': ['eth0'], (...)}")
+    :return: the same value but with a proper type
+    """
+    try:
+        return ast.literal_eval(value)
+    except ValueError:
+        return str(value)
+
+
 def formatvalue(value):
     if isinstance(value, str):
         return value
@@ -121,15 +133,3 @@ def yield_or_stop(generator):
             yield next(generator)
         except (EmptyResponseError, ConnectionError, HTTPError, StopIteration):
             return
-
-
-def is_bool(b):
-    try:
-        bool(strtobool(b))
-        return True
-    except ValueError:
-        return False
-    except TypeError:
-        return False
-    except AttributeError:
-        return False
diff --git a/test/test_utils.py b/test/test_utils.py
index 00fbf3b..e9cc1c5 100644
--- a/test/test_utils.py
+++ b/test/test_utils.py
@@ -228,30 +228,3 @@ def test_stop_http_error():
     gen = utils.yield_or_stop(my_generator())
     for val in gen:
         assert 1 == val
-
-
-def test_is_bool_false():
-    test_values = [
-        "Test",
-        "SomeWrongData",
-        2,
-        2.0,
-        -5
-    ]
-
-    for test_value in test_values:
-        assert utils.is_bool(test_value) is False
-
-
-def test_is_bool_true():
-    test_values = [
-        "True",
-        "true",
-        "False",
-        "false",
-        "1",
-        "0"
-    ]
-
-    for test_value in test_values:
-        assert utils.is_bool(test_value) is True