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.


Discussion:

Resolving patch:
diff --git a/nikola/post.py b/nikola/post.py
--- a/nikola/post.py
+++ b/nikola/post.py
@@ -54,7 +54,8 @@
     to_datetime,
     demote_headers,
     get_translation_candidate,
-    map_metadata
+    map_metadata,
+    bool_from_meta,
 )
 
 try:
@@ -427,13 +428,14 @@ def __repr__(self):
 
     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 @@ def has_math(self):
             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
--- a/nikola/utils.py
+++ b/nikola/utils.py
@@ -97,7 +97,7 @@
            '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
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -20,6 +20,7 @@
     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()