Repository: searxng/searxng

Issue:
Preferences options aren't consistent.
<!-- PLEASE FILL THESE FIELDS, IT REALLY HELPS THE MAINTAINERS OF SearXNG -->

**Version of SearXNG, commit number if you are using on master branch and stipulate if you forked SearXNG**
<!-- If you are running on master branch using git execute this command
in order to fetch the latest commit ID:
```
git log -1
``` 
If you are using searxng-docker then look at the bottom of the SearXNG page
and check for the version after "Powered by SearXNG"

Please also stipulate if you are using a forked version of SearXNG and
include a link to the fork source code.
-->
2022.07.04-7a9beb4f 
**How did you install SearXNG?**
<!-- Did you install SearXNG using the official wiki or using searxng-docker
or manually by executing the searx/webapp.py file? -->
Used https://searx.tiekoetter.com/ instance
**What happened?**
Some options have checkboxes, some- "On" "Off" selection menus, while others have "Disabled" "Enabled".

**How To Reproduce**
<!-- How can we reproduce this issue? (as minimally and as precisely as possible) -->
Go to options page, settings have these three types of the same type of setting (On or Off).
**Expected behavior**
<!-- A clear and concise description of what you expected to happen. -->
Settings should have the same representation of the same type of option.
**Screenshots & Logs**
![image](https://user-images.githubusercontent.com/79922836/177176728-eb7cfc4e-a61e-4bcb-bf96-5f09db284384.png)
![image](https://user-images.githubusercontent.com/79922836/177176756-0cb961ff-d0fd-4f5b-ae2c-f9807d143019.png)



**Additional context**
<!-- Add any other context about the problem here. -->



Discussion:
Send https://github.com/searxng/searxng/pull/1540 .. if you have time, please give it a try / thanks!

Resolving patch:
diff --git a/searx/preferences.py b/searx/preferences.py
--- a/searx/preferences.py
+++ b/searx/preferences.py
@@ -9,6 +9,7 @@
 from zlib import compress, decompress
 from urllib.parse import parse_qs, urlencode
 from typing import Iterable, Dict, List, Optional
+from collections import OrderedDict
 
 import flask
 import babel
@@ -24,6 +25,18 @@
 COOKIE_MAX_AGE = 60 * 60 * 24 * 365 * 5  # 5 years
 DOI_RESOLVERS = list(settings['doi_resolvers'])
 
+MAP_STR2BOOL: Dict[str, bool] = OrderedDict(
+    [
+        ('0', False),
+        ('1', True),
+        ('on', True),
+        ('off', False),
+        ('True', True),
+        ('False', False),
+        ('none', False),
+    ]
+)
+
 
 class ValidationException(Exception):
 
@@ -199,6 +212,26 @@ def save(self, name: str, resp: flask.Response):
             resp.set_cookie(name, self.key, max_age=COOKIE_MAX_AGE)
 
 
+class BooleanSetting(Setting):
+    """Setting of a boolean value that has to be translated in order to be storable"""
+
+    def normalized_str(self, val):
+        for v_str, v_obj in MAP_STR2BOOL.items():
+            if val == v_obj:
+                return v_str
+        raise ValueError("Invalid value: %s (%s) is not a boolean!" % (repr(val), type(val)))
+
+    def parse(self, data: str):
+        """Parse and validate ``data`` and store the result at ``self.value``"""
+        self.value = MAP_STR2BOOL[data]
+        self.key = self.normalized_str(self.value)  # pylint: disable=attribute-defined-outside-init
+
+    def save(self, name: str, resp: flask.Response):
+        """Save cookie ``name`` in the HTTP response object"""
+        if hasattr(self, 'key'):
+            resp.set_cookie(name, self.key, max_age=COOKIE_MAX_AGE)
+
+
 class BooleanChoices:
     """Maps strings to booleans that are either true or false."""
 
@@ -375,16 +408,9 @@ def __init__(
                 locked=is_locked('autocomplete'),
                 choices=list(autocomplete.backends.keys()) + ['']
             ),
-            'image_proxy': MapSetting(
+            'image_proxy': BooleanSetting(
                 settings['server']['image_proxy'],
-                locked=is_locked('image_proxy'),
-                map={
-                    '': settings['server']['image_proxy'],
-                    '0': False,
-                    '1': True,
-                    'True': True,
-                    'False': False
-                }
+                locked=is_locked('image_proxy')
             ),
             'method': EnumStringSetting(
                 settings['server']['method'],
@@ -405,15 +431,9 @@ def __init__(
                 locked=is_locked('theme'),
                 choices=themes
             ),
-            'results_on_new_tab': MapSetting(
+            'results_on_new_tab': BooleanSetting(
                 settings['ui']['results_on_new_tab'],
-                locked=is_locked('results_on_new_tab'),
-                map={
-                    '0': False,
-                    '1': True,
-                    'False': False,
-                    'True': True
-                }
+                locked=is_locked('results_on_new_tab')
             ),
             'doi_resolver': MultipleChoiceSetting(
                 [settings['default_doi_resolver'], ],
@@ -425,48 +445,21 @@ def __init__(
                 locked=is_locked('simple_style'),
                 choices=['', 'auto', 'light', 'dark']
             ),
-            'center_alignment': MapSetting(
+            'center_alignment': BooleanSetting(
                 settings['ui']['center_alignment'],
-                locked=is_locked('center_alignment'),
-                map={
-                    '0': False,
-                    '1': True,
-                    'False': False,
-                    'True': True
-                }
+                locked=is_locked('center_alignment')
             ),
-            'advanced_search': MapSetting(
+            'advanced_search': BooleanSetting(
                 settings['ui']['advanced_search'],
-                locked=is_locked('advanced_search'),
-                map={
-                    '0': False,
-                    '1': True,
-                    'False': False,
-                    'True': True,
-                    'on': True,
-                }
+                locked=is_locked('advanced_search')
             ),
-            'query_in_title': MapSetting(
+            'query_in_title': BooleanSetting(
                 settings['ui']['query_in_title'],
-                locked=is_locked('query_in_title'),
-                map={
-                    '': settings['ui']['query_in_title'],
-                    '0': False,
-                    '1': True,
-                    'True': True,
-                    'False': False
-                }
+                locked=is_locked('query_in_title')
             ),
-            'infinite_scroll': MapSetting(
+            'infinite_scroll': BooleanSetting(
                 settings['ui']['infinite_scroll'],
-                locked=is_locked('infinite_scroll'),
-                map={
-                    '': settings['ui']['infinite_scroll'],
-                    '0': False,
-                    '1': True,
-                    'True': True,
-                    'False': False
-                }
+                locked=is_locked('infinite_scroll')
             ),
             # fmt: on
         }
@@ -529,6 +522,13 @@ def parse_form(self, input_data: Dict[str, str]):
         disabled_engines = []
         enabled_categories = []
         disabled_plugins = []
+
+        # boolean preferences are not sent by the form if they're false,
+        # so we have to add them as false manually if they're not sent (then they would be true)
+        for key, setting in self.key_value_settings.items():
+            if key not in input_data.keys() and isinstance(setting, BooleanSetting):
+                input_data[key] = 'False'
+
         for user_setting_name, user_setting in input_data.items():
             if user_setting_name in self.key_value_settings:
                 self.key_value_settings[user_setting_name].parse(user_setting)


Test patch: