Merge pull request #70 from matejc/theming_support
add multi theming support
							
								
								
									
										4
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						| @ -44,13 +44,13 @@ minimal: bin/buildout minimal.cfg setup.py | |||||||
| 	bin/buildout -c minimal.cfg $(options) | 	bin/buildout -c minimal.cfg $(options) | ||||||
| 
 | 
 | ||||||
| styles: | styles: | ||||||
| 	@lessc -x searx/static/less/style.less > searx/static/css/style.css | 	@lessc -x searx/static/default/less/style.less > searx/static/default/css/style.css | ||||||
| 
 | 
 | ||||||
| locales: | locales: | ||||||
| 	@pybabel compile -d searx/translations | 	@pybabel compile -d searx/translations | ||||||
| 
 | 
 | ||||||
| clean: | clean: | ||||||
| 	@rm -rf .installed.cfg .mr.developer.cfg bin parts develop-eggs \
 | 	@rm -rf .installed.cfg .mr.developer.cfg bin parts develop-eggs \
 | ||||||
| 		searx.egg-info lib include .coverage coverage searx/static/css/*.css | 		searx.egg-info lib include .coverage coverage searx/static/default/css/*.css | ||||||
| 
 | 
 | ||||||
| .PHONY: all tests robot flake8 coverage production minimal styles locales clean | .PHONY: all tests robot flake8 coverage production minimal styles locales clean | ||||||
|  | |||||||
| @ -4,6 +4,8 @@ server: | |||||||
|     debug : True |     debug : True | ||||||
|     request_timeout : 2.0 # seconds |     request_timeout : 2.0 # seconds | ||||||
|     base_url : False |     base_url : False | ||||||
|  |     themes_path : "" | ||||||
|  |     default_theme : default | ||||||
| 
 | 
 | ||||||
| engines: | engines: | ||||||
|   - name : wikipedia |   - name : wikipedia | ||||||
|  | |||||||
| Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB | 
| Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 7.6 KiB | 
| Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.4 KiB | 
| Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB | 
| Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB | 
| Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB | 
| Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.4 KiB | 
| Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB | 
| Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB | 
| Before Width: | Height: | Size: 837 B After Width: | Height: | Size: 837 B | 
| Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB | 
| Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB | 
| Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 7.1 KiB | 
| @ -1,6 +1,6 @@ | |||||||
| {% extends 'base.html' %} | {% extends 'default/base.html' %} | ||||||
| {% block content %} | {% block content %} | ||||||
| {% include 'github_ribbon.html' %} | {% include 'default/github_ribbon.html' %} | ||||||
| <div class="row"> | <div class="row"> | ||||||
|     <h1>About <a href="{{ url_for('index') }}">searx</a></h1> |     <h1>About <a href="{{ url_for('index') }}">searx</a></h1> | ||||||
| 
 | 
 | ||||||
| @ -1,8 +1,8 @@ | |||||||
| {% extends "base.html" %} | {% extends "default/base.html" %} | ||||||
| {% block content %} | {% block content %} | ||||||
| <div class="center"> | <div class="center"> | ||||||
|     <div class="title"><h1>searx</h1></div> |     <div class="title"><h1>searx</h1></div> | ||||||
|     {% include 'search.html' %} |     {% include 'default/search.html' %} | ||||||
|     <p class="top_margin"> |     <p class="top_margin"> | ||||||
|         <a href="{{ url_for('about') }}" class="hmarg">{{ _('about') }}</a> |         <a href="{{ url_for('about') }}" class="hmarg">{{ _('about') }}</a> | ||||||
|         <a href="{{ url_for('preferences') }}" class="hmarg">{{ _('preferences') }}</a> |         <a href="{{ url_for('preferences') }}" class="hmarg">{{ _('preferences') }}</a> | ||||||
| @ -1,4 +1,4 @@ | |||||||
| {% extends "base.html" %} | {% extends "default/base.html" %} | ||||||
| {% block head %} {% endblock %} | {% block head %} {% endblock %} | ||||||
| {% block content %} | {% block content %} | ||||||
| <div class="row"> | <div class="row"> | ||||||
| @ -8,7 +8,7 @@ | |||||||
|     <fieldset> |     <fieldset> | ||||||
|         <legend>{{ _('Default categories') }}</legend> |         <legend>{{ _('Default categories') }}</legend> | ||||||
|         <p> |         <p> | ||||||
|         {% include 'categories.html' %} |         {% include 'default/categories.html' %} | ||||||
|         </p> |         </p> | ||||||
|     </fieldset> |     </fieldset> | ||||||
|     <fieldset> |     <fieldset> | ||||||
| @ -52,6 +52,16 @@ | |||||||
|         </select> |         </select> | ||||||
|         </p> |         </p> | ||||||
|     </fieldset> |     </fieldset> | ||||||
|  |     <fieldset> | ||||||
|  |         <legend>{{ _('Themes') }}</legend> | ||||||
|  |         <p> | ||||||
|  |         <select name="theme"> | ||||||
|  |             {% for name in themes %} | ||||||
|  |             <option value="{{ name }}" {% if name == theme %}selected="selected"{% endif %}>{{ name }}</option> | ||||||
|  |             {% endfor %} | ||||||
|  |         </select> | ||||||
|  |         </p> | ||||||
|  |     </fieldset> | ||||||
|     <fieldset> |     <fieldset> | ||||||
|     <legend>{{ _('Currently used search engines') }}</legend> |     <legend>{{ _('Currently used search engines') }}</legend> | ||||||
| 
 | 
 | ||||||
| @ -1,7 +1,7 @@ | |||||||
| <div class="result {{ result.class }}"> | <div class="result {{ result.class }}"> | ||||||
| 
 | 
 | ||||||
|   {% if result['favicon'] %} |   {% if result['favicon'] %} | ||||||
|     <img width="14" height="14" class="favicon" src="static/img/icon_{{result['favicon']}}.ico" /> |     <img width="14" height="14" class="favicon" src="static/{{theme}}/img/icon_{{result['favicon']}}.ico" /> | ||||||
|   {% endif %} |   {% endif %} | ||||||
| 
 | 
 | ||||||
|   <div> |   <div> | ||||||
| @ -1,6 +1,6 @@ | |||||||
| <div class="result"> | <div class="result"> | ||||||
|   {% if result['favicon'] %} |   {% if result['favicon'] %} | ||||||
|     <img width="14" height="14" class="favicon" src="static/img/icon_{{result['favicon']}}.ico" /> |     <img width="14" height="14" class="favicon" src="static/{{theme}}/img/icon_{{result['favicon']}}.ico" /> | ||||||
|   {% endif %} |   {% endif %} | ||||||
| 
 | 
 | ||||||
|     <p> |     <p> | ||||||
| @ -1,9 +1,9 @@ | |||||||
| {% extends "base.html" %} | {% extends "default/base.html" %} | ||||||
| {% block title %}{{ q }} - {% endblock %} | {% block title %}{{ q }} - {% endblock %} | ||||||
| {% block content %} | {% block content %} | ||||||
| <div class="right"><a href="{{ url_for('preferences') }}" id="preferences"><span>preferences</span></a></div> | <div class="right"><a href="{{ url_for('preferences') }}" id="preferences"><span>preferences</span></a></div> | ||||||
| <div class="small search center"> | <div class="small search center"> | ||||||
|     {% include 'search.html' %} |     {% include 'default/search.html' %} | ||||||
| </div> | </div> | ||||||
| <div id="results"> | <div id="results"> | ||||||
|     <div id="sidebar"> |     <div id="sidebar"> | ||||||
| @ -43,9 +43,9 @@ | |||||||
| 
 | 
 | ||||||
|     {% for result in results %} |     {% for result in results %} | ||||||
|         {% if result['template'] %} |         {% if result['template'] %} | ||||||
|             {% include 'result_templates/'+result['template'] %} |             {% include 'default/result_templates/'+result['template'] %} | ||||||
|         {% else %} |         {% else %} | ||||||
|             {% include 'result_templates/default.html' %} |             {% include 'default/result_templates/default.html' %} | ||||||
|         {% endif %} |         {% endif %} | ||||||
|     {% endfor %} |     {% endfor %} | ||||||
| 
 | 
 | ||||||
| @ -3,5 +3,5 @@ | |||||||
|     <input type="text" placeholder="{{ _('Search for...') }}" id="q" class="q" name="q" tabindex="1" autocomplete="off" {% if q %}value="{{ q }}"{% endif %}/> |     <input type="text" placeholder="{{ _('Search for...') }}" id="q" class="q" name="q" tabindex="1" autocomplete="off" {% if q %}value="{{ q }}"{% endif %}/> | ||||||
|     <input type="submit" value="search" id="search_submit" /> |     <input type="submit" value="search" id="search_submit" /> | ||||||
|   </div> |   </div> | ||||||
|   {% include 'categories.html' %} |   {% include 'default/categories.html' %} | ||||||
| </form> | </form> | ||||||
| @ -1,4 +1,4 @@ | |||||||
| {% extends "base.html" %} | {% extends "default/base.html" %} | ||||||
| {% block head %} {% endblock %} | {% block head %} {% endblock %} | ||||||
| {% block content %} | {% block content %} | ||||||
| <h2>{{ _('Engine stats') }}</h2> | <h2>{{ _('Engine stats') }}</h2> | ||||||
| @ -1,11 +1,13 @@ | |||||||
| from HTMLParser import HTMLParser |  | ||||||
| #import htmlentitydefs | #import htmlentitydefs | ||||||
| import csv |  | ||||||
| from codecs import getincrementalencoder | from codecs import getincrementalencoder | ||||||
| import cStringIO | from HTMLParser import HTMLParser | ||||||
| import re |  | ||||||
| from random import choice | from random import choice | ||||||
| 
 | 
 | ||||||
|  | import cStringIO | ||||||
|  | import csv | ||||||
|  | import os | ||||||
|  | import re | ||||||
|  | 
 | ||||||
| ua_versions = ('26.0', '27.0', '28.0') | ua_versions = ('26.0', '27.0', '28.0') | ||||||
| ua_os = ('Windows NT 6.3; WOW64', | ua_os = ('Windows NT 6.3; WOW64', | ||||||
|          'X11; Linux x86_64', |          'X11; Linux x86_64', | ||||||
| @ -110,3 +112,17 @@ class UnicodeWriter: | |||||||
|     def writerows(self, rows): |     def writerows(self, rows): | ||||||
|         for row in rows: |         for row in rows: | ||||||
|             self.writerow(row) |             self.writerow(row) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def get_themes(root): | ||||||
|  |     """Returns available themes list.""" | ||||||
|  | 
 | ||||||
|  |     static_path = os.path.join(root, 'static') | ||||||
|  |     static_names = set(os.listdir(static_path)) | ||||||
|  |     templates_path = os.path.join(root, 'templates') | ||||||
|  |     templates_names = set(os.listdir(templates_path)) | ||||||
|  | 
 | ||||||
|  |     themes = [] | ||||||
|  |     for name in static_names.intersection(templates_names): | ||||||
|  |         themes += [name] | ||||||
|  |     return static_path, templates_path, themes | ||||||
|  | |||||||
| @ -38,16 +38,23 @@ from searx.engines import ( | |||||||
|     search as do_search, categories, engines, get_engines_stats, |     search as do_search, categories, engines, get_engines_stats, | ||||||
|     engine_shortcuts |     engine_shortcuts | ||||||
| ) | ) | ||||||
| from searx.utils import UnicodeWriter, highlight_content, html_to_text | from searx.utils import ( | ||||||
|  |     UnicodeWriter, highlight_content, html_to_text, get_themes | ||||||
|  | ) | ||||||
| from searx.languages import language_codes | from searx.languages import language_codes | ||||||
| from searx.search import Search | from searx.search import Search | ||||||
| from searx.autocomplete import backends as autocomplete_backends | from searx.autocomplete import backends as autocomplete_backends | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | static_path, templates_path, themes = get_themes(settings['themes_path'] if \ | ||||||
|  |     settings.get('themes_path', None) else searx_dir) | ||||||
|  | default_theme = settings['default_theme'] if \ | ||||||
|  |     settings.get('default_theme', None) else 'default' | ||||||
|  | 
 | ||||||
| app = Flask( | app = Flask( | ||||||
|     __name__, |     __name__, | ||||||
|     static_folder=os.path.join(searx_dir, 'static'), |     static_folder=static_path, | ||||||
|     template_folder=os.path.join(searx_dir, 'templates') |     template_folder=templates_path | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| app.secret_key = settings['server']['secret_key'] | app.secret_key = settings['server']['secret_key'] | ||||||
| @ -90,7 +97,30 @@ def get_base_url(): | |||||||
|     return hostname |     return hostname | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def render(template_name, **kwargs): | def get_current_theme_name(override=None): | ||||||
|  |     """Returns theme name. | ||||||
|  | 
 | ||||||
|  |     Checks in this order: | ||||||
|  |     1. override | ||||||
|  |     2. cookies | ||||||
|  |     3. settings""" | ||||||
|  | 
 | ||||||
|  |     if override and override in themes: | ||||||
|  |         return override | ||||||
|  |     theme_name = request.cookies.get('theme', default_theme) | ||||||
|  |     if theme_name not in themes: | ||||||
|  |         theme_name = default_theme | ||||||
|  |     return theme_name | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def url_for_theme(endpoint, override_theme=None, **values): | ||||||
|  |     if endpoint == 'static' and values.get('filename', None): | ||||||
|  |         theme_name = get_current_theme_name(override=override_theme) | ||||||
|  |         values['filename'] = "{}/{}".format(theme_name, values['filename']) | ||||||
|  |     return url_for(endpoint, **values) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def render(template_name, override_theme=None, **kwargs): | ||||||
|     blocked_engines = request.cookies.get('blocked_engines', '').split(',') |     blocked_engines = request.cookies.get('blocked_engines', '').split(',') | ||||||
| 
 | 
 | ||||||
|     autocomplete = request.cookies.get('autocomplete') |     autocomplete = request.cookies.get('autocomplete') | ||||||
| @ -125,7 +155,13 @@ def render(template_name, **kwargs): | |||||||
| 
 | 
 | ||||||
|     kwargs['method'] = request.cookies.get('method', 'POST') |     kwargs['method'] = request.cookies.get('method', 'POST') | ||||||
| 
 | 
 | ||||||
|     return render_template(template_name, **kwargs) |     # override url_for function in templates | ||||||
|  |     kwargs['url_for'] = url_for_theme | ||||||
|  | 
 | ||||||
|  |     kwargs['theme'] = get_current_theme_name(override=override_theme) | ||||||
|  | 
 | ||||||
|  |     return render_template( | ||||||
|  |         '{}/{}'.format(kwargs['theme'], template_name), **kwargs) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @app.route('/search', methods=['GET', 'POST']) | @app.route('/search', methods=['GET', 'POST']) | ||||||
| @ -232,7 +268,8 @@ def index(): | |||||||
|         paging=search.paging, |         paging=search.paging, | ||||||
|         pageno=search.pageno, |         pageno=search.pageno, | ||||||
|         base_url=get_base_url(), |         base_url=get_base_url(), | ||||||
|         suggestions=search.suggestions |         suggestions=search.suggestions, | ||||||
|  |         theme=get_current_theme_name() | ||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @ -290,7 +327,7 @@ def preferences(): | |||||||
| 
 | 
 | ||||||
|     if request.method == 'GET': |     if request.method == 'GET': | ||||||
|         blocked_engines = request.cookies.get('blocked_engines', '').split(',') |         blocked_engines = request.cookies.get('blocked_engines', '').split(',') | ||||||
|     else: |     else:  # on save | ||||||
|         selected_categories = [] |         selected_categories = [] | ||||||
|         locale = None |         locale = None | ||||||
|         autocomplete = '' |         autocomplete = '' | ||||||
| @ -315,6 +352,8 @@ def preferences(): | |||||||
|                 engine_name = pd_name.replace('engine_', '', 1) |                 engine_name = pd_name.replace('engine_', '', 1) | ||||||
|                 if engine_name in engines: |                 if engine_name in engines: | ||||||
|                     blocked_engines.append(engine_name) |                     blocked_engines.append(engine_name) | ||||||
|  |             elif pd_name == 'theme': | ||||||
|  |                 theme = pd if pd in themes else default_theme | ||||||
| 
 | 
 | ||||||
|         resp = make_response(redirect(url_for('index'))) |         resp = make_response(redirect(url_for('index'))) | ||||||
| 
 | 
 | ||||||
| @ -352,6 +391,9 @@ def preferences(): | |||||||
| 
 | 
 | ||||||
|         resp.set_cookie('method', method, max_age=cookie_max_age) |         resp.set_cookie('method', method, max_age=cookie_max_age) | ||||||
| 
 | 
 | ||||||
|  |         resp.set_cookie( | ||||||
|  |             'theme', theme, max_age=cookie_max_age) | ||||||
|  | 
 | ||||||
|         return resp |         return resp | ||||||
|     return render('preferences.html', |     return render('preferences.html', | ||||||
|                   locales=settings['locales'], |                   locales=settings['locales'], | ||||||
| @ -361,7 +403,9 @@ def preferences(): | |||||||
|                   categs=categories.items(), |                   categs=categories.items(), | ||||||
|                   blocked_engines=blocked_engines, |                   blocked_engines=blocked_engines, | ||||||
|                   autocomplete_backends=autocomplete_backends, |                   autocomplete_backends=autocomplete_backends, | ||||||
|                   shortcuts={y: x for x, y in engine_shortcuts.items()}) |                   shortcuts={y: x for x, y in engine_shortcuts.items()}, | ||||||
|  |                   themes=themes, | ||||||
|  |                   theme=get_current_theme_name()) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @app.route('/stats', methods=['GET']) | @app.route('/stats', methods=['GET']) | ||||||
| @ -404,7 +448,10 @@ def opensearch(): | |||||||
| 
 | 
 | ||||||
| @app.route('/favicon.ico') | @app.route('/favicon.ico') | ||||||
| def favicon(): | def favicon(): | ||||||
|     return send_from_directory(os.path.join(app.root_path, 'static/img'), |     return send_from_directory(os.path.join(app.root_path, | ||||||
|  |                                             'static', | ||||||
|  |                                             get_current_theme_name(), | ||||||
|  |                                             'img'), | ||||||
|                                'favicon.png', |                                'favicon.png', | ||||||
|                                mimetype='image/vnd.microsoft.icon') |                                mimetype='image/vnd.microsoft.icon') | ||||||
| 
 | 
 | ||||||
|  | |||||||