on master help sign in
Merge pull request #534 from kvch/preferences-refactor

new preferences handling

commit149b08a062fd002c3cb52bfe423ead23495ae8df authorAdam Tauber <asciimoo@gmail.com> dateSat 9 Apr 2016, 17:33 (11 years ago) parent9331fc2f0fd9ad
8 files changed+525−168
searx/preferences.py+269 searx/search.py+5−10 searx/settings_robot.yml+1−1 searx/utils.py−23 searx/webapp.py+36−133 tests/robot/test_basic.robot+108 tests/unit/test_preferences.py+101 tests/unit/test_webapp.py+5−1
searx/preferences.pynew file+269
@@ -0,0 +1,269 @@
1+from searx import settings, autocomplete
2+from searx.languages import language_codes as languages
3+
4+
5+COOKIE_MAX_AGE = 60 * 60 * 24 * 365 * 5 # 5 years
6+LANGUAGE_CODES = [l[0] for l in languages]
7+LANGUAGE_CODES.append('all')
8+DISABLED = 0
9+ENABLED = 1
10+
11+
12+class MissingArgumentException(Exception):
13+ pass
14+
15+
16+class ValidationException(Exception):
17+ pass
18+
19+
20+class Setting(object):
21+ """Base class of user settings"""
22+
23+ def __init__(self, default_value, **kwargs):
24+ super(Setting, self).__init__()
25+ self.value = default_value
26+ for key, value in kwargs.iteritems():
27+ setattr(self, key, value)
28+
29+ self._post_init()
30+
31+ def _post_init(self):
32+ pass
33+
34+ def parse(self, data):
35+ self.value = data
36+
37+ def get_value(self):
38+ return self.value
39+
40+ def save(self, name, resp):
41+ resp.set_cookie(name, bytes(self.value), max_age=COOKIE_MAX_AGE)
42+
43+
44+class StringSetting(Setting):
45+ """Setting of plain string values"""
46+ pass
47+
48+
49+class EnumStringSetting(Setting):
50+ """Setting of a value which can only come from the given choices"""
51+
52+ def _post_init(self):
53+ if not hasattr(self, 'choices'):
54+ raise MissingArgumentException('Missing argument: choices')
55+
56+ if self.value != '' and self.value not in self.choices:
57+ raise ValidationException('Invalid default value: {0}'.format(self.value))
58+
59+ def parse(self, data):
60+ if data not in self.choices and data != self.value:
61+ raise ValidationException('Invalid choice: {0}'.format(data))
62+ self.value = data
63+
64+
65+class MultipleChoiceSetting(EnumStringSetting):
66+ """Setting of values which can only come from the given choices"""
67+
68+ def _post_init(self):
69+ if not hasattr(self, 'choices'):
70+ raise MissingArgumentException('Missing argument: choices')
71+ for item in self.value:
72+ if item not in self.choices:
73+ raise ValidationException('Invalid default value: {0}'.format(self.value))
74+
75+ def parse(self, data):
76+ if data == '':
77+ self.value = []
78+ return
79+
80+ elements = data.split(',')
81+ for item in elements:
82+ if item not in self.choices:
83+ raise ValidationException('Invalid choice: {0}'.format(item))
84+ self.value = elements
85+
86+ def parse_form(self, data):
87+ self.value = []
88+ for choice in data:
89+ if choice in self.choices and choice not in self.value:
90+ self.value.append(choice)
91+
92+ def save(self, name, resp):
93+ resp.set_cookie(name, ','.join(self.value), max_age=COOKIE_MAX_AGE)
94+
95+
96+class MapSetting(Setting):
97+ """Setting of a value that has to be translated in order to be storable"""
98+
99+ def _post_init(self):
100+ if not hasattr(self, 'map'):
101+ raise MissingArgumentException('missing argument: map')
102+ if self.value not in self.map.values():
103+ raise ValidationException('Invalid default value')
104+
105+ def parse(self, data):
106+ if data not in self.map:
107+ raise ValidationException('Invalid choice: {0}'.format(data))
108+ self.value = self.map[data]
109+ self.key = data
110+
111+ def save(self, name, resp):
112+ resp.set_cookie(name, bytes(self.key), max_age=COOKIE_MAX_AGE)
113+
114+
115+class SwitchableSetting(Setting):
116+ """ Base class for settings that can be turned on && off"""
117+
118+ def _post_init(self):
119+ self.disabled = set()
120+ self.enabled = set()
121+ if not hasattr(self, 'choices'):
122+ raise MissingArgumentException('missing argument: choices')
123+
124+ def transform_form_items(self, items):
125+ return items
126+
127+ def transform_values(self, values):
128+ return values
129+
130+ def parse_cookie(self, data):
131+ if data[DISABLED] != '':
132+ self.disabled = set(data[DISABLED].split(','))
133+ if data[ENABLED] != '':
134+ self.enabled = set(data[ENABLED].split(','))
135+
136+ def parse_form(self, items):
137+ items = self.transform_form_items(items)
138+
139+ self.disabled = set()
140+ self.enabled = set()
141+ for choice in self.choices:
142+ if choice['default_on']:
143+ if choice['id'] in items:
144+ self.disabled.add(choice['id'])
145+ else:
146+ if choice['id'] not in items:
147+ self.enabled.add(choice['id'])
148+
149+ def save(self, resp):
150+ resp.set_cookie('disabled_{0}'.format(self.value), ','.join(self.disabled), max_age=COOKIE_MAX_AGE)
151+ resp.set_cookie('enabled_{0}'.format(self.value), ','.join(self.enabled), max_age=COOKIE_MAX_AGE)
152+
153+ def get_disabled(self):
154+ disabled = self.disabled
155+ for choice in self.choices:
156+ if not choice['default_on'] and choice['id'] not in self.enabled:
157+ disabled.add(choice['id'])
158+ return self.transform_values(disabled)
159+
160+ def get_enabled(self):
161+ enabled = self.enabled
162+ for choice in self.choices:
163+ if choice['default_on'] and choice['id'] not in self.disabled:
164+ enabled.add(choice['id'])
165+ return self.transform_values(enabled)
166+
167+
168+class EnginesSetting(SwitchableSetting):
169+ def _post_init(self):
170+ super(EnginesSetting, self)._post_init()
171+ transformed_choices = []
172+ for engine_name, engine in self.choices.iteritems():
173+ for category in engine.categories:
174+ transformed_choice = dict()
175+ transformed_choice['default_on'] = not engine.disabled
176+ transformed_choice['id'] = '{}__{}'.format(engine_name, category)
177+ transformed_choices.append(transformed_choice)
178+ self.choices = transformed_choices
179+
180+ def transform_form_items(self, items):
181+ return [item[len('engine_'):].replace('_', ' ').replace(' ', '__') for item in items]
182+
183+ def transform_values(self, values):
184+ if len(values) == 1 and values[0] == '':
185+ return list()
186+ transformed_values = []
187+ for value in values:
188+ engine, category = value.split('__')
189+ transformed_values.append((engine, category))
190+ return transformed_values
191+
192+
193+class PluginsSetting(SwitchableSetting):
194+ def _post_init(self):
195+ super(PluginsSetting, self)._post_init()
196+ transformed_choices = []
197+ for plugin in self.choices:
198+ transformed_choice = dict()
199+ transformed_choice['default_on'] = plugin.default_on
200+ transformed_choice['id'] = plugin.id
201+ transformed_choices.append(transformed_choice)
202+ self.choices = transformed_choices
203+
204+ def transform_form_items(self, items):
205+ return [item[len('plugin_'):] for item in items]
206+
207+
208+class Preferences(object):
209+ """Stores, validates and saves preferences to cookies"""
210+
211+ def __init__(self, themes, categories, engines, plugins):
212+ super(Preferences, self).__init__()
213+
214+ self.key_value_settings = {'categories': MultipleChoiceSetting(['general'], choices=categories),
215+ 'language': EnumStringSetting('all', choices=LANGUAGE_CODES),
216+ 'locale': EnumStringSetting(settings['ui']['default_locale'],
217+ choices=settings['locales'].keys()),
218+ 'autocomplete': EnumStringSetting(settings['search']['autocomplete'],
219+ choices=autocomplete.backends.keys()),
220+ 'image_proxy': MapSetting(settings['server']['image_proxy'],
221+ map={'': settings['server']['image_proxy'],
222+ '0': False,
223+ '1': True}),
224+ 'method': EnumStringSetting('POST', choices=('GET', 'POST')),
225+ 'safesearch': MapSetting(settings['search']['safe_search'], map={'0': 0,
226+ '1': 1,
227+ '2': 2}),
228+ 'theme': EnumStringSetting(settings['ui']['default_theme'], choices=themes)}
229+
230+ self.engines = EnginesSetting('engines', choices=engines)
231+ self.plugins = PluginsSetting('plugins', choices=plugins)
232+
233+ def parse_cookies(self, input_data):
234+ for user_setting_name, user_setting in input_data.iteritems():
235+ if user_setting_name in self.key_value_settings:
236+ self.key_value_settings[user_setting_name].parse(user_setting)
237+ elif user_setting_name == 'disabled_engines':
238+ self.engines.parse_cookie([input_data['disabled_engines'], input_data['enabled_engines']])
239+ elif user_setting_name == 'disabled_plugins':
240+ self.plugins.parse_cookie([input_data['disabled_plugins'], input_data['enabled_plugins']])
241+
242+ def parse_form(self, input_data):
243+ disabled_engines = []
244+ enabled_categories = []
245+ disabled_plugins = []
246+ for user_setting_name, user_setting in input_data.iteritems():
247+ if user_setting_name in self.key_value_settings:
248+ self.key_value_settings[user_setting_name].parse(user_setting)
249+ elif user_setting_name.startswith('engine_'):
250+ disabled_engines.append(user_setting_name)
251+ elif user_setting_name.startswith('category_'):
252+ enabled_categories.append(user_setting_name[len('category_'):])
253+ elif user_setting_name.startswith('plugin_'):
254+ disabled_plugins.append(user_setting_name)
255+ self.key_value_settings['categories'].parse_form(enabled_categories)
256+ self.engines.parse_form(disabled_engines)
257+ self.plugins.parse_form(disabled_plugins)
258+
259+ # cannot be used in case of engines or plugins
260+ def get_value(self, user_setting_name):
261+ if user_setting_name in self.key_value_settings:
262+ return self.key_value_settings[user_setting_name].get_value()
263+
264+ def save(self, resp):
265+ for user_setting_name, user_setting in self.key_value_settings.iteritems():
266+ user_setting.save(user_setting_name, resp)
267+ self.engines.save(resp)
268+ self.plugins.save(resp)
269+ return resp
searx/search.py+5−10
@@ -23,7 +23,7 @@ from searx.engines import (
2323 categories, engines
2424)
2525from searx.languages import language_codes
26−from searx.utils import gen_useragent, get_blocked_engines
26+from searx.utils import gen_useragent
2727from searx.query import Query
2828from searx.results import ResultContainer
2929from searx import logger
@@ -140,15 +140,13 @@ class Search(object):
140140 self.lang = 'all'
141141
142142 # set blocked engines
143− self.blocked_engines = get_blocked_engines(engines, request.cookies)
143+ self.blocked_engines = request.preferences.engines.get_disabled()
144144
145145 self.result_container = ResultContainer()
146146 self.request_data = {}
147147
148148 # set specific language if set
149− if request.cookies.get('language')\
150− and request.cookies['language'] in (x[0] for x in language_codes):
151− self.lang = request.cookies['language']
149+ self.lang = request.preferences.get_value('language')
152150
153151 # set request method
154152 if request.method == 'POST':
@@ -294,11 +292,8 @@ class Search(object):
294292 else:
295293 request_params['language'] = self.lang
296294
297− try:
298− # 0 = None, 1 = Moderate, 2 = Strict
299− request_params['safesearch'] = int(request.cookies.get('safesearch'))
300− except Exception:
301− request_params['safesearch'] = settings['search']['safe_search']
295+ # 0 = None, 1 = Moderate, 2 = Strict
296+ request_params['safesearch'] = request.preferences.get_value('safesearch')
302297
303298 # update request parameters dependent on
304299 # search-engine (contained in engines folder)
searx/settings_robot.yml+1−1
@@ -4,7 +4,7 @@ general:
44
55search:
66 safe_search : 0
7− autocomplete : 0
7+ autocomplete : ""
88
99server:
1010 port : 11111
searx/utils.py−23
@@ -230,26 +230,3 @@ def list_get(a_list, index, default=None):
230230 return a_list[index]
231231 else:
232232 return default
233−
234−
235−def get_blocked_engines(engines, cookies):
236− if 'blocked_engines' not in cookies:
237− return [(engine_name, category) for engine_name in engines
238− for category in engines[engine_name].categories if engines[engine_name].disabled]
239−
240− blocked_engine_strings = cookies.get('blocked_engines', '').split(',')
241− blocked_engines = []
242−
243− if not blocked_engine_strings:
244− return blocked_engines
245−
246− for engine_string in blocked_engine_strings:
247− if engine_string.find('__') > -1:
248− engine, category = engine_string.split('__', 1)
249− if engine in engines and category in engines[engine].categories:
250− blocked_engines.append((engine, category))
251− elif engine_string in engines:
252− for category in engines[engine_string].categories:
253− blocked_engines.append((engine_string, category))
254−
255− return blocked_engines
searx/webapp.py+36−133
@@ -56,7 +56,7 @@ from searx.engines import (
5656from searx.utils import (
5757 UnicodeWriter, highlight_content, html_to_text, get_themes,
5858 get_static_files, get_result_templates, gen_useragent, dict_subset,
59− prettify_url, get_blocked_engines
59+ prettify_url
6060)
6161from searx.version import VERSION_STRING
6262from searx.languages import language_codes
@@ -64,6 +64,7 @@ from searx.search import Search
6464from searx.query import Query
6565from searx.autocomplete import searx_bang, backends as autocomplete_backends
6666from searx.plugins import plugins
67+from searx.preferences import Preferences
6768
6869# check if the pyopenssl, ndg-httpsclient, pyasn1 packages are installed.
6970# They are needed for SSL connection without trouble, see #298
@@ -109,8 +110,6 @@ for indice, theme in enumerate(themes):
109110 for (dirpath, dirnames, filenames) in os.walk(theme_img_path):
110111 global_favicons[indice].extend(filenames)
111112
112−cookie_max_age = 60 * 60 * 24 * 365 * 5 # 5 years
113−
114113_category_names = (gettext('files'),
115114 gettext('general'),
116115 gettext('music'),
@@ -222,9 +221,7 @@ def get_current_theme_name(override=None):
222221
223222 if override and override in themes:
224223 return override
225− theme_name = request.args.get('theme',
226− request.cookies.get('theme',
227− default_theme))
224+ theme_name = request.args.get('theme', request.preferences.get_value('theme'))
228225 if theme_name not in themes:
229226 theme_name = default_theme
230227 return theme_name
@@ -262,12 +259,8 @@ def image_proxify(url):
262259
263260
264261def render(template_name, override_theme=None, **kwargs):
265− blocked_engines = get_blocked_engines(engines, request.cookies)
266−
267− autocomplete = request.cookies.get('autocomplete', settings['search']['autocomplete'])
268−
269− if autocomplete not in autocomplete_backends:
270− autocomplete = None
262+ blocked_engines = request.preferences.engines.get_disabled()
263+ autocomplete = request.preferences.get_value('autocomplete')
271264
272265 nonblocked_categories = set(category for engine_name in engines
273266 for category in engines[engine_name].categories
@@ -295,7 +288,7 @@ def render(template_name, override_theme=None, **kwargs):
295288 kwargs['selected_categories'].append(c)
296289
297290 if not kwargs['selected_categories']:
298− cookie_categories = request.cookies.get('categories', '').split(',')
291+ cookie_categories = request.preferences.get_value('categories')
299292 for ccateg in cookie_categories:
300293 if ccateg in categories:
301294 kwargs['selected_categories'].append(ccateg)
@@ -311,9 +304,9 @@ def render(template_name, override_theme=None, **kwargs):
311304
312305 kwargs['searx_version'] = VERSION_STRING
313306
314− kwargs['method'] = request.cookies.get('method', 'POST')
307+ kwargs['method'] = request.preferences.get_value('method')
315308
316− kwargs['safesearch'] = request.cookies.get('safesearch', str(settings['search']['safe_search']))
309+ kwargs['safesearch'] = str(request.preferences.get_value('safesearch'))
317310
318311 # override url_for function in templates
319312 kwargs['url_for'] = url_for_theme
@@ -347,14 +340,18 @@ def render(template_name, override_theme=None, **kwargs):
347340@app.before_request
348341def pre_request():
349342 # merge GET, POST vars
343+ preferences = Preferences(themes, categories.keys(), engines, plugins)
344+ preferences.parse_cookies(request.cookies)
345+ request.preferences = preferences
346+
350347 request.form = dict(request.form.items())
351348 for k, v in request.args.items():
352349 if k not in request.form:
353350 request.form[k] = v
354351
355352 request.user_plugins = []
356− allowed_plugins = request.cookies.get('allowed_plugins', '').split(',')
357− disabled_plugins = request.cookies.get('disabled_plugins', '').split(',')
353+ allowed_plugins = preferences.plugins.get_enabled()
354+ disabled_plugins = preferences.plugins.get_disabled()
358355 for plugin in plugins:
359356 if ((plugin.default_on and plugin.id not in disabled_plugins)
360357 or plugin.id in allowed_plugins):
@@ -486,7 +483,7 @@ def autocompleter():
486483 request_data = request.args
487484
488485 # set blocked engines
489− blocked_engines = get_blocked_engines(engines, request.cookies)
486+ blocked_engines = request.preferences.engines.get_disabled()
490487
491488 # parse query
492489 query = Query(request_data.get('q', '').encode('utf-8'), blocked_engines)
@@ -496,8 +493,8 @@ def autocompleter():
496493 if not query.getSearchQuery():
497494 return '', 400
498495
499− # get autocompleter
500− completer = autocomplete_backends.get(request.cookies.get('autocomplete', settings['search']['autocomplete']))
496+ # run autocompleter
497+ completer = autocomplete_backends.get(request.preferences.get_value('autocomplete'))
501498
502499 # parse searx specific autocompleter results like !bang
503500 raw_results = searx_bang(query)
@@ -532,117 +529,23 @@ def autocompleter():
532529
533530@app.route('/preferences', methods=['GET', 'POST'])
534531def preferences():
535− """Render preferences page.
536−
537− Settings that are going to be saved as cookies."""
538− lang = None
539− image_proxy = request.cookies.get('image_proxy', settings['server'].get('image_proxy'))
540−
541− if request.cookies.get('language')\
542− and request.cookies['language'] in (x[0] for x in language_codes):
543− lang = request.cookies['language']
544−
545− blocked_engines = []
546−
547− resp = make_response(redirect(urljoin(settings['server']['base_url'], url_for('index'))))
548−
549− if request.method == 'GET':
550− blocked_engines = get_blocked_engines(engines, request.cookies)
551− else: # on save
552− selected_categories = []
553− post_disabled_plugins = []
554− locale = None
555− autocomplete = ''
556− method = 'POST'
557− safesearch = settings['search']['safe_search']
558− for pd_name, pd in request.form.items():
559− if pd_name.startswith('category_'):
560− category = pd_name[9:]
561− if category not in categories:
562− continue
563− selected_categories.append(category)
564− elif pd_name == 'locale' and pd in settings['locales']:
565− locale = pd
566− elif pd_name == 'image_proxy':
567− image_proxy = pd
568− elif pd_name == 'autocomplete':
569− autocomplete = pd
570− elif pd_name == 'language' and (pd == 'all' or
571− pd in (x[0] for
572− x in language_codes)):
573− lang = pd
574− elif pd_name == 'method':
575− method = pd
576− elif pd_name == 'safesearch':
577− safesearch = pd
578− elif pd_name.startswith('engine_'):
579− if pd_name.find('__') > -1:
580− # TODO fix underscore vs space
581− engine_name, category = [x.replace('_', ' ') for x in
582− pd_name.replace('engine_', '', 1).split('__', 1)]
583− if engine_name in engines and category in engines[engine_name].categories:
584− blocked_engines.append((engine_name, category))
585− elif pd_name == 'theme':
586− theme = pd if pd in themes else default_theme
587− elif pd_name.startswith('plugin_'):
588− plugin_id = pd_name.replace('plugin_', '', 1)
589− if not any(plugin.id == plugin_id for plugin in plugins):
590− continue
591− post_disabled_plugins.append(plugin_id)
592− else:
593− resp.set_cookie(pd_name, pd, max_age=cookie_max_age)
594−
595− disabled_plugins = []
596− allowed_plugins = []
597− for plugin in plugins:
598− if plugin.default_on:
599− if plugin.id in post_disabled_plugins:
600− disabled_plugins.append(plugin.id)
601− elif plugin.id not in post_disabled_plugins:
602− allowed_plugins.append(plugin.id)
603−
604− resp.set_cookie('disabled_plugins', ','.join(disabled_plugins), max_age=cookie_max_age)
532+ """Render preferences page && save user preferences"""
605533
606− resp.set_cookie('allowed_plugins', ','.join(allowed_plugins), max_age=cookie_max_age)
607−
608− resp.set_cookie(
609− 'blocked_engines', ','.join('__'.join(e) for e in blocked_engines),
610− max_age=cookie_max_age
611− )
612−
613− if locale:
614− resp.set_cookie(
615− 'locale', locale,
616− max_age=cookie_max_age
617− )
618−
619− if lang:
620− resp.set_cookie(
621− 'language', lang,
622− max_age=cookie_max_age
623− )
624−
625− if selected_categories:
626− # cookie max age: 4 weeks
627− resp.set_cookie(
628− 'categories', ','.join(selected_categories),
629− max_age=cookie_max_age
630− )
631−
632− resp.set_cookie(
633− 'autocomplete', autocomplete,
634− max_age=cookie_max_age
635− )
636−
637− resp.set_cookie('method', method, max_age=cookie_max_age)
638−
639− resp.set_cookie('safesearch', str(safesearch), max_age=cookie_max_age)
640−
641− resp.set_cookie('image_proxy', image_proxy, max_age=cookie_max_age)
642−
643− resp.set_cookie('theme', theme, max_age=cookie_max_age)
644−
645− return resp
534+ # save preferences
535+ if request.method == 'POST':
536+ resp = make_response(redirect(urljoin(settings['server']['base_url'], url_for('index'))))
537+ try:
538+ request.preferences.parse_form(request.form)
539+ except ValidationException:
540+ # TODO use flash feature of flask
541+ return resp
542+ return request.preferences.save(resp)
543+
544+ # render preferences
545+ image_proxy = request.preferences.get_value('image_proxy')
546+ lang = request.preferences.get_value('language')
547+ blocked_engines = request.preferences.engines.get_disabled()
548+ allowed_plugins = request.preferences.plugins.get_enabled()
646549
647550 # stats for preferences page
648551 stats = {}
@@ -664,7 +567,7 @@ def preferences():
664567 return render('preferences.html',
665568 locales=settings['locales'],
666569 current_locale=get_locale(),
667− current_language=lang or 'all',
570+ current_language=lang,
668571 image_proxy=image_proxy,
669572 language_codes=language_codes,
670573 engines_by_category=categories,
@@ -674,7 +577,7 @@ def preferences():
674577 shortcuts={y: x for x, y in engine_shortcuts.items()},
675578 themes=themes,
676579 plugins=plugins,
677− allowed_plugins=[plugin.id for plugin in request.user_plugins],
580+ allowed_plugins=allowed_plugins,
678581 theme=get_current_theme_name())
679582
680583
@@ -750,7 +653,7 @@ Disallow: /preferences
750653def opensearch():
751654 method = 'post'
752655
753− if request.cookies.get('method', 'POST') == 'GET':
656+ if request.preferences.get_value('method') == 'GET':
754657 method = 'get'
755658
756659 # chrome/chromium only supports HTTP GET....
tests/robot/test_basic.robot+108
@@ -42,3 +42,111 @@ Change language
4242 Location Should Be http://localhost:11111/
4343 Page Should Contain rólunk
4444 Page Should Contain beállítások
45+
46+Change method
47+ Page Should Contain about
48+ Page Should Contain preferences
49+ Go To http://localhost:11111/preferences
50+ Select From List method GET
51+ Submit Form id=search_form
52+ Location Should Be http://localhost:11111/
53+ Go To http://localhost:11111/preferences
54+ List Selection Should Be method GET
55+ Select From List method POST
56+ Submit Form id=search_form
57+ Location Should Be http://localhost:11111/
58+ Go To http://localhost:11111/preferences
59+ List Selection Should Be method POST
60+
61+Change theme
62+ Page Should Contain about
63+ Page Should Contain preferences
64+ Go To http://localhost:11111/preferences
65+ List Selection Should Be theme default
66+ Select From List theme oscar
67+ Submit Form id=search_form
68+ Location Should Be http://localhost:11111/
69+ Go To http://localhost:11111/preferences
70+ List Selection Should Be theme oscar
71+
72+Change safesearch
73+ Page Should Contain about
74+ Page Should Contain preferences
75+ Go To http://localhost:11111/preferences
76+ List Selection Should Be safesearch None
77+ Select From List safesearch Strict
78+ Submit Form id=search_form
79+ Location Should Be http://localhost:11111/
80+ Go To http://localhost:11111/preferences
81+ List Selection Should Be safesearch Strict
82+
83+Change image proxy
84+ Page Should Contain about
85+ Page Should Contain preferences
86+ Go To http://localhost:11111/preferences
87+ List Selection Should Be image_proxy Disabled
88+ Select From List image_proxy Enabled
89+ Submit Form id=search_form
90+ Location Should Be http://localhost:11111/
91+ Go To http://localhost:11111/preferences
92+ List Selection Should Be image_proxy Enabled
93+
94+Change search language
95+ Page Should Contain about
96+ Page Should Contain preferences
97+ Go To http://localhost:11111/preferences
98+ List Selection Should Be language Automatic
99+ Select From List language Turkish (Turkey) - tr_TR
100+ Submit Form id=search_form
101+ Location Should Be http://localhost:11111/
102+ Go To http://localhost:11111/preferences
103+ List Selection Should Be language Turkish (Turkey) - tr_TR
104+
105+Change autocomplete
106+ Page Should Contain about
107+ Page Should Contain preferences
108+ Go To http://localhost:11111/preferences
109+ List Selection Should Be autocomplete -
110+ Select From List autocomplete google
111+ Submit Form id=search_form
112+ Location Should Be http://localhost:11111/
113+ Go To http://localhost:11111/preferences
114+ List Selection Should Be autocomplete google
115+
116+Change allowed/disabled engines
117+ Page Should Contain about
118+ Page Should Contain preferences
119+ Go To http://localhost:11111/preferences
120+ Page Should Contain Engine name
121+ Element Should Contain xpath=//label[@class="deny"][@for='engine_dummy_dummy_dummy'] Block
122+ Element Should Contain xpath=//label[@class="deny"][@for='engine_general_general_dummy'] Block
123+ Click Element xpath=//label[@class="deny"][@for='engine_general_general_dummy']
124+ Submit Form id=search_form
125+ Location Should Be http://localhost:11111/
126+ Page Should Contain about
127+ Page Should Contain preferences
128+ Go To http://localhost:11111/preferences
129+ Page Should Contain Engine name
130+ Element Should Contain xpath=//label[@class="deny"][@for='engine_dummy_dummy_dummy'] Block
131+ Element Should Contain xpath=//label[@class="deny"][@for='engine_general_general_dummy'] \
132+
133+Block a plugin
134+ Page Should Contain about
135+ Page Should Contain preferences
136+ Go To http://localhost:11111/preferences
137+ List Selection Should Be theme default
138+ Select From List theme oscar
139+ Submit Form id=search_form
140+ Location Should Be http://localhost:11111/
141+ Go To http://localhost:11111/preferences
142+ List Selection Should Be theme oscar
143+ Page Should Contain Plugins
144+ Click Link Plugins
145+ Checkbox Should Not Be Selected id=plugin_HTTPS_rewrite
146+ Click Element xpath=//label[@for='plugin_HTTPS_rewrite']
147+ Submit Form id=search_form
148+ Location Should Be http://localhost:11111/
149+ Go To http://localhost:11111/preferences
150+ Page Should Contain Plugins
151+ Click Link Plugins
152+ Checkbox Should Be Selected id=plugin_HTTPS_rewrite
tests/unit/test_preferences.pynew file+101
@@ -0,0 +1,101 @@
1+from searx.preferences import (EnumStringSetting, MapSetting, MissingArgumentException,
2+ MultipleChoiceSetting, PluginsSetting, ValidationException)
3+from searx.testing import SearxTestCase
4+
5+
6+class PluginStub(object):
7+ def __init__(self, id, default_on):
8+ self.id = id
9+ self.default_on = default_on
10+
11+
12+class TestSettings(SearxTestCase):
13+ # map settings
14+ def test_map_setting_invalid_initialization(self):
15+ with self.assertRaises(MissingArgumentException):
16+ setting = MapSetting(3, wrong_argument={'0': 0})
17+
18+ def test_map_setting_invalid_default_value(self):
19+ with self.assertRaises(ValidationException):
20+ setting = MapSetting(3, map={'dog': 1, 'bat': 2})
21+
22+ def test_map_setting_invalid_choice(self):
23+ setting = MapSetting(2, map={'dog': 1, 'bat': 2})
24+ with self.assertRaises(ValidationException):
25+ setting.parse('cat')
26+
27+ def test_map_setting_valid_default(self):
28+ setting = MapSetting(3, map={'dog': 1, 'bat': 2, 'cat': 3})
29+ self.assertEquals(setting.get_value(), 3)
30+
31+ def test_map_setting_valid_choice(self):
32+ setting = MapSetting(3, map={'dog': 1, 'bat': 2, 'cat': 3})
33+ self.assertEquals(setting.get_value(), 3)
34+ setting.parse('bat')
35+ self.assertEquals(setting.get_value(), 2)
36+
37+ def test_enum_setting_invalid_initialization(self):
38+ with self.assertRaises(MissingArgumentException):
39+ setting = EnumStringSetting('cat', wrong_argument=[0, 1, 2])
40+
41+ # enum settings
42+ def test_enum_setting_invalid_initialization(self):
43+ with self.assertRaises(MissingArgumentException):
44+ setting = EnumStringSetting('cat', wrong_argument=[0, 1, 2])
45+
46+ def test_enum_setting_invalid_default_value(self):
47+ with self.assertRaises(ValidationException):
48+ setting = EnumStringSetting(3, choices=[0, 1, 2])
49+
50+ def test_enum_setting_invalid_choice(self):
51+ setting = EnumStringSetting(0, choices=[0, 1, 2])
52+ with self.assertRaises(ValidationException):
53+ setting.parse(3)
54+
55+ def test_enum_setting_valid_default(self):
56+ setting = EnumStringSetting(3, choices=[1, 2, 3])
57+ self.assertEquals(setting.get_value(), 3)
58+
59+ def test_enum_setting_valid_choice(self):
60+ setting = EnumStringSetting(3, choices=[1, 2, 3])
61+ self.assertEquals(setting.get_value(), 3)
62+ setting.parse(2)
63+ self.assertEquals(setting.get_value(), 2)
64+
65+ # multiple choice settings
66+ def test_multiple_setting_invalid_initialization(self):
67+ with self.assertRaises(MissingArgumentException):
68+ setting = MultipleChoiceSetting(['2'], wrong_argument=['0', '1', '2'])
69+
70+ def test_multiple_setting_invalid_default_value(self):
71+ with self.assertRaises(ValidationException):
72+ setting = MultipleChoiceSetting(['3', '4'], choices=['0', '1', '2'])
73+
74+ def test_multiple_setting_invalid_choice(self):
75+ setting = MultipleChoiceSetting(['1', '2'], choices=['0', '1', '2'])
76+ with self.assertRaises(ValidationException):
77+ setting.parse('4, 3')
78+
79+ def test_multiple_setting_valid_default(self):
80+ setting = MultipleChoiceSetting(['3'], choices=['1', '2', '3'])
81+ self.assertEquals(setting.get_value(), ['3'])
82+
83+ def test_multiple_setting_valid_choice(self):
84+ setting = MultipleChoiceSetting(['3'], choices=['1', '2', '3'])
85+ self.assertEquals(setting.get_value(), ['3'])
86+ setting.parse('2')
87+ self.assertEquals(setting.get_value(), ['2'])
88+
89+ # plugins settings
90+ def test_plugins_setting_all_default_enabled(self):
91+ plugin1 = PluginStub('plugin1', True)
92+ plugin2 = PluginStub('plugin2', True)
93+ setting = PluginsSetting(['3'], choices=[plugin1, plugin2])
94+ self.assertEquals(setting.get_enabled(), set(['plugin1', 'plugin2']))
95+
96+ def test_plugins_setting_few_default_enabled(self):
97+ plugin1 = PluginStub('plugin1', True)
98+ plugin2 = PluginStub('plugin2', False)
99+ plugin3 = PluginStub('plugin3', True)
100+ setting = PluginsSetting('name', choices=[plugin1, plugin2, plugin3])
101+ self.assertEquals(setting.get_enabled(), set(['plugin1', 'plugin3']))
tests/unit/test_webapp.py+5−1
@@ -12,7 +12,6 @@ class ViewsTestCase(SearxTestCase):
1212 def setUp(self):
1313 webapp.app.config['TESTING'] = True # to get better error messages
1414 self.app = webapp.app.test_client()
15− webapp.default_theme = 'default'
1615
1716 # set some defaults
1817 self.test_results = [
@@ -43,6 +42,11 @@ class ViewsTestCase(SearxTestCase):
4342
4443 webapp.Search.search = search_mock
4544
45+ def get_current_theme_name_mock(override=None):
46+ return 'default'
47+
48+ webapp.get_current_theme_name = get_current_theme_name_mock
49+
4650 self.maxDiff = None # to see full diffs
4751
4852 def test_index_empty(self):