Repository: getnikola/nikola

Issue:
unexpected pretty_url behavior
<!--
Before creating an issue:
* make sure you are using an up-to-date version of Nikola
* search for existing issues that might be related

Make sure to:
* provide information about your environment (below)
* include all the output you get, and any other information related to your problem

Nikola v7.6.4, as provided by Ubuntu, is NOT SUPPORTED.
If you are using this version, you should upgrade: https://getnikola.com/getting-started.html
-->

### Environment

**Python Version:** 3.8

**Nikola Version:** 8.1.2

**Operating System:** macOS

### Description:

In some situation, setting `PRETTY_URLS = True` in `conf.py` and `pretty_url: false` does not turn off `pretty_url` for that page, e.g. for a file `pages/404.md`.

Even more strangely, if I set `pretty_url: 'some random string'`, it would then turn off `pretty_url`.

I'm guessing it may be related to a multi-lingual setup.

MWE for `pages/404.md`:

```markdown
---
title: sajgfhadfg
pretty_url: false
# or
pretty_url: 'some random string'
url_type: full_path
---

sdfhasdjkgfhsdfalkjg
```

MWE for `conf.py` not available yet.

Comments:

Patch:
diff --git a/CHANGES.txt b/CHANGES.txt
index 565f272e6..6dcdf0f3a 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -9,6 +9,8 @@ Features
 * Add ``BasePlugin.register_auto_watched_folder()``
 * Allow different ``PANDOC_OPTIONS`` values based on input extensions
   by specifying a dict of ``{".extension": [options]}`` (Issue #3492)
+* Allow boolean/integer ``pretty_url`` post meta values in YAML/TOML
+  (Issue #3503)
 
 Bugfixes
 --------
diff --git a/nikola/post.py b/nikola/post.py
index b58f181ca..795f9d60f 100644
--- a/nikola/post.py
+++ b/nikola/post.py
@@ -54,7 +54,8 @@ from .utils import (
     to_datetime,
     demote_headers,
     get_translation_candidate,
-    map_metadata
+    map_metadata,
+    bool_from_meta,
 )
 
 try:
@@ -427,13 +428,14 @@ class Post(object):
 
     def has_pretty_url(self, lang):
         """Check if this page has a pretty URL."""
-        m = self.meta[lang].get('pretty_url', '')
-        if m:
-            # match is a non-empty string, overides anything
-            return m.lower() == 'true' or m.lower() == 'yes'
-        else:
+        meta_value = bool_from_meta(self.meta[lang], 'pretty_url')
+
+        if meta_value is None:
             # use PRETTY_URLS, unless the slug is 'index'
             return self.pretty_urls and self.meta[lang]['slug'] != 'index'
+        else:
+            # override with meta value
+            return meta_value
 
     def _has_pretty_url(self, lang):
         """Check if this page has a pretty URL."""
@@ -450,13 +452,13 @@ class Post(object):
             return True
         lang = nikola.utils.LocaleBorg().current_lang
         if self.is_translation_available(lang):
-            if self.meta[lang].get('has_math') in ('true', 'True', 'yes', '1', 1, True):
+            if bool_from_meta(self.meta[lang], 'has_math'):
                 return True
             if self.config['USE_TAG_METADATA']:
                 return 'mathjax' in self.tags_for_language(lang)
         # If it has math in ANY other language, enable it. Better inefficient than broken.
         for lang in self.translated_to:
-            if self.meta[lang].get('has_math') in ('true', 'True', 'yes', '1', 1, True):
+            if bool_from_meta(self.meta[lang], 'has_math'):
                 return True
         if self.config['USE_TAG_METADATA']:
             return 'mathjax' in self.alltags
diff --git a/nikola/utils.py b/nikola/utils.py
index 82e99d6bd..54cd36f68 100644
--- a/nikola/utils.py
+++ b/nikola/utils.py
@@ -97,7 +97,7 @@ __all__ = ('CustomEncoder', 'get_theme_path', 'get_theme_path_real',
            'adjust_name_for_index_path', 'adjust_name_for_index_link',
            'NikolaPygmentsHTML', 'create_redirect', 'clean_before_deployment',
            'sort_posts', 'smartjoin', 'indent', 'load_data', 'html_unescape',
-           'rss_writer', 'map_metadata', 'req_missing',
+           'rss_writer', 'map_metadata', 'req_missing', 'bool_from_meta',
            # Deprecated, moved to hierarchy_utils:
            'TreeNode', 'clone_treenode', 'flatten_tree_structure',
            'sort_classifications', 'join_hierarchical_category_path',
@@ -1497,6 +1497,24 @@ def write_metadata(data, metadata_format=None, comment_wrap=False, site=None, co
         return DEFAULT_EXTRACTOR.write_metadata(data, comment_wrap)
 
 
+def bool_from_meta(meta, key, fallback=False, blank=None):
+    """Convert a boolean-ish meta value to a boolean."""
+    value = meta.get(key)
+    if isinstance(value, str):
+        value_lowercase = value.lower().strip()
+        if value_lowercase in {"true", "yes", "1"}:
+            return True
+        elif value_lowercase in {"false", "no", "0"}:
+            return False
+        elif not value_lowercase:
+            return blank
+    elif isinstance(value, int):
+        return bool(value)
+    elif value is None:
+        return blank
+    return fallback
+
+
 def ask(query, default=None):
     """Ask a question."""
     if default:


Test patch:
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 997d52068..19966790c 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -20,6 +20,7 @@ from nikola.utils import (
     get_theme_chain,
     get_translation_candidate,
     write_metadata,
+    bool_from_meta,
 )
 
 
@@ -573,6 +574,35 @@ def test_write_metadata_fallbacks(post, arg):
     assert write_metadata(data, arg) == ".. title: xx\n\n"
 
 
+@pytest.mark.parametrize("value, expected", [
+    ("true", True),
+    ("True", True),
+    ("TRUE", True),
+    ("yes", True),
+    ("Yes", True),
+    ("YES", True),
+    ("false", False),
+    ("False", False),
+    ("FALSE", False),
+    ("no", False),
+    ("No", False),
+    ("NO", False),
+    ("1", True),
+    (1, True),
+    ("0", False),
+    (0, False),
+    ("0", False),
+    (True, True),
+    (False, False),
+    ("unknown", "F"),
+    (None, "B"),
+    ("", "B"),
+])
+def test_bool_from_meta(value, expected):
+    meta = {"key": value}
+    assert bool_from_meta(meta, "key", "F", "B") == expected
+
+
 @pytest.fixture
 def post():
     return FakePost()