Repository: ckan/ckan

Issue:
`ckan.lib.helpers.format_resource_items` discards `False` boolean values
**CKAN version**
2.9

**Describe the bug**
I have boolean resource metadata keys defined via schemas in my DCOR instance. These are not written to the resources HTML page, because [format_resource_items](https://github.com/ckan/ckan/blob/c5c529d10ebe63d8573515483fdd46e0839477f0/ckan/lib/helpers.py#L2294) discards `False` boolean values.

**Steps to reproduce**
Set a value of a resource key to `False`.

**Expected behavior**
Value should be displayed on HTML resource page.

**Additional details**
This can be fixed by changing the line linked above

```python
if not value or key in blacklist: 
    continue
```

 to

```python
if not isinstance(value, bool) and (not value or key in blacklist): 
    continue
```

**I can write a PR including a test for `format_resource_items` if you give me green light.**
`ckan.lib.helpers.format_resource_items` discards `False` boolean values
**CKAN version**
2.9

**Describe the bug**
I have boolean resource metadata keys defined via schemas in my DCOR instance. These are not written to the resources HTML page, because [format_resource_items](https://github.com/ckan/ckan/blob/c5c529d10ebe63d8573515483fdd46e0839477f0/ckan/lib/helpers.py#L2294) discards `False` boolean values.

**Steps to reproduce**
Set a value of a resource key to `False`.

**Expected behavior**
Value should be displayed on HTML resource page.

**Additional details**
This can be fixed by changing the line linked above

```python
if not value or key in blacklist: 
    continue
```

 to

```python
if not isinstance(value, bool) and (not value or key in blacklist): 
    continue
```

**I can write a PR including a test for `format_resource_items` if you give me green light.**


Discussion:
I think the helper expects strings, even though there are some handling for special cases like dates and numbers. But the actual type given to the function is still a string.
I thought `items` (what `format_resource_items` accepts)  would be something like `dict.items()`, i.e. a list of `(key, value)` pairs.

And no, (while the keys are strings) the values are definitely not always strings. They can be floats, integers, and boolean. Otherwise it would not make sense to handle those cases in that function.

https://github.com/ckan/ckan/blob/c5c529d10ebe63d8573515483fdd46e0839477f0/ckan/lib/helpers.py#L2293-L2317
@paulmueller it makes sense to handle boolean as special case as well. A PR would be great!
I think the helper expects strings, even though there are some handling for special cases like dates and numbers. But the actual type given to the function is still a string.
I thought `items` (what `format_resource_items` accepts)  would be something like `dict.items()`, i.e. a list of `(key, value)` pairs.

And no, (while the keys are strings) the values are definitely not always strings. They can be floats, integers, and boolean. Otherwise it would not make sense to handle those cases in that function.

https://github.com/ckan/ckan/blob/c5c529d10ebe63d8573515483fdd46e0839477f0/ckan/lib/helpers.py#L2293-L2317
@paulmueller it makes sense to handle boolean as special case as well. A PR would be great!

Resolving patch:
diff --git a/ckan/lib/helpers.py b/ckan/lib/helpers.py
--- a/ckan/lib/helpers.py
+++ b/ckan/lib/helpers.py
@@ -8,6 +8,7 @@
 import email.utils
 import datetime
 import logging
+import numbers
 import re
 import os
 import pytz
@@ -2291,7 +2292,11 @@ def format_resource_items(items):
     reg_ex_int = r'^-?\d{1,}$'
     reg_ex_float = r'^-?\d{1,}\.\d{1,}$'
     for key, value in items:
-        if not value or key in blacklist:
+        if (key in blacklist
+                or (not isinstance(value, numbers.Number) and not value)):
+            # Ignore blocked keys and values that evaluate to
+            # `bool(value) == False` (e.g. `""`, `[]` or `{}`),
+            # with the exception of numbers such as `False`, `0`,`0.0`.
             continue
         # size is treated specially as we want to show in MiB etc
         if key == 'size':
@@ -2310,8 +2315,9 @@ def format_resource_items(items):
                 value = formatters.localised_number(float(value))
             elif re.search(reg_ex_int, value):
                 value = formatters.localised_number(int(value))
-        elif ((isinstance(value, int) or isinstance(value, float))
-                and value not in (True, False)):
+        elif isinstance(value, bool):
+            value = str(value)
+        elif isinstance(value, numbers.Number):
             value = formatters.localised_number(value)
         key = key.replace('_', ' ')
         output.append((key, value))


Test patch:
diff --git a/ckan/tests/lib/test_helpers.py b/ckan/tests/lib/test_helpers.py
--- a/ckan/tests/lib/test_helpers.py
+++ b/ckan/tests/lib/test_helpers.py
@@ -8,6 +8,7 @@
 import pytz
 import tzlocal
 from babel import Locale
+import flask_babel
 
 import pytest
 
@@ -914,6 +915,25 @@ def test_date_str_to_datetime_invalid(string):
         h.date_str_to_datetime(string)
 
 
+@pytest.mark.parametrize("dict_in,dict_out", [
+    ({"number_bool": True}, {"number bool": "True"}),
+    ({"number_bool": False}, {"number bool": "False"}),
+    ({"number_int": 0}, {"number int": "0"}),
+    ({"number_int": 42}, {"number int": "42"}),
+    ({"number_float": 0.0}, {"number float": "0"}),
+    ({"number_float": 0.1}, {"number float": "0.1"}),
+    ({"number_float": "0.10"}, {"number float": "0.1"}),
+    ({"string_basic": "peter"}, {"string basic": "peter"}),
+    ({"string_empty": ""}, {}),  # empty strings are ignored
+    ({"name": "hans"}, {}),  # blocked string
+])
+def test_format_resource_items_data_types(dict_in, dict_out, monkeypatch):
+    # set locale to en (formatting of decimals)
+    monkeypatch.setattr(flask_babel, "get_locale", lambda: "en")
+    items_out = h.format_resource_items(dict_in.items())
+    assert items_out == list(dict_out.items())
+
+
 def test_gravatar():
     email = "zephod@gmail.com"
     expected = '<img src="//gravatar.com/avatar/7856421db6a63efa5b248909c472fbd2?s=200&amp;d=mm"'