From 7f8a3849c797b27a3c84ffcdec3483951a5bc458 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Fri, 29 May 2015 19:44:11 +0200 Subject: [PATCH] Adjusted messages.js compilation, debugged new i18n handling and added logging --- src/octoprint/server/__init__.py | 611 +++++------------------------ src/octoprint/server/util/flask.py | 81 +++- src/octoprint/server/views.py | 473 ++++++++++++++++++++++ src/octoprint/settings.py | 3 +- 4 files changed, 642 insertions(+), 526 deletions(-) create mode 100644 src/octoprint/server/views.py diff --git a/src/octoprint/server/__init__.py b/src/octoprint/server/__init__.py index 6f9e5f19..43fd5247 100644 --- a/src/octoprint/server/__init__.py +++ b/src/octoprint/server/__init__.py @@ -7,7 +7,7 @@ __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms import uuid from sockjs.tornado import SockJSRouter -from flask import Flask, render_template, send_from_directory, g, request, make_response, session, url_for +from flask import Flask, g, request, session from flask.ext.login import LoginManager, current_user from flask.ext.principal import Principal, Permission, RoleNeed, identity_loaded, UserNeed from flask.ext.babel import Babel, gettext, ngettext @@ -24,7 +24,7 @@ SUCCESS = {} NO_CONTENT = ("", 204) app = Flask("octoprint") -babel = Babel(app) +babel = None debug = False printer = None @@ -58,9 +58,6 @@ import octoprint.filemanager.analysis import octoprint.slicing from . import util -util.tornado.fix_ioloop_scheduling() -util.flask.enable_plugin_translations() - UI_API_KEY = ''.join('%02X' % ord(z) for z in uuid.uuid4().bytes) @@ -71,481 +68,8 @@ DISPLAY_VERSION = "%s (%s branch)" % (VERSION, BRANCH) if BRANCH else VERSION del versions -def get_available_locale_identifiers(locales): - result = set() - - # add available translations - for locale in locales: - result.add(locale.language) - if locale.territory: - # if a territory is specified, add that too - result.add("%s_%s" % (locale.language, locale.territory)) - - return result - - -LOCALES = [Locale.parse("en")] + babel.list_translations() -LANGUAGES = get_available_locale_identifiers(LOCALES) - - -@app.before_request -def before_request(): - g.locale = get_locale() - -@app.after_request -def after_request(response): - # send no-cache headers with all POST responses - if request.method == "POST": - response.cache_control.no_cache = True - response.headers.add("X-Clacks-Overhead", "GNU Terry Pratchett") - return response - - -@babel.localeselector -def get_locale(): - if "l10n" in request.values: - return Locale.negotiate([request.values["l10n"]], LANGUAGES) - - if hasattr(g, "identity") and g.identity and userManager is not None: - userid = g.identity.id - try: - user_language = userManager.getUserSetting(userid, ("interface", "language")) - if user_language is not None and not user_language == "_default": - return Locale.negotiate([user_language], LANGUAGES) - except octoprint.users.UnknownUser: - pass - - default_language = settings().get(["appearance", "defaultLanguage"]) - if default_language is not None and not default_language == "_default" and default_language in LANGUAGES: - return Locale.negotiate([default_language], LANGUAGES) - - return request.accept_languages.best_match(LANGUAGES) - - -@app.route("/") -@util.flask.cached(refreshif=lambda: util.flask.cache_check_headers() or "_refresh" in request.values, key=lambda: "view/%s/%s" % (request.path, g.locale)) -def index(): - - #~~ a bunch of settings - - enable_gcodeviewer = settings().getBoolean(["gcodeViewer", "enabled"]) - enable_timelapse = (settings().get(["webcam", "snapshot"]) and settings().get(["webcam", "ffmpeg"])) - enable_systemmenu = settings().get(["system"]) is not None and settings().get(["system", "actions"]) is not None and len(settings().get(["system", "actions"])) > 0 - enable_accesscontrol = userManager is not None - preferred_stylesheet = settings().get(["devel", "stylesheet"]) - locales = dict((l.language, dict(language=l.language, display=l.display_name, english=l.english_name)) for l in LOCALES) - -#~~ prepare assets - - supported_stylesheets = ("css", "less") - assets = dict( - js=[], - stylesheets=[] - ) - assets["js"] = [ - url_for('static', filename='js/app/viewmodels/appearance.js'), - url_for('static', filename='js/app/viewmodels/connection.js'), - url_for('static', filename='js/app/viewmodels/control.js'), - url_for('static', filename='js/app/viewmodels/firstrun.js'), - url_for('static', filename='js/app/viewmodels/files.js'), - url_for('static', filename='js/app/viewmodels/loginstate.js'), - url_for('static', filename='js/app/viewmodels/navigation.js'), - url_for('static', filename='js/app/viewmodels/printerstate.js'), - url_for('static', filename='js/app/viewmodels/printerprofiles.js'), - url_for('static', filename='js/app/viewmodels/settings.js'), - url_for('static', filename='js/app/viewmodels/slicing.js'), - url_for('static', filename='js/app/viewmodels/temperature.js'), - url_for('static', filename='js/app/viewmodels/terminal.js'), - url_for('static', filename='js/app/viewmodels/users.js'), - url_for('static', filename='js/app/viewmodels/log.js'), - url_for('static', filename='js/app/viewmodels/usersettings.js') - ] - if enable_gcodeviewer: - assets["js"] += [ - url_for('static', filename='js/app/viewmodels/gcode.js'), - url_for('static', filename='gcodeviewer/js/ui.js'), - url_for('static', filename='gcodeviewer/js/gCodeReader.js'), - url_for('static', filename='gcodeviewer/js/renderer.js') - ] - if enable_timelapse: - assets["js"].append(url_for('static', filename='js/app/viewmodels/timelapse.js')) - - if preferred_stylesheet == "less": - assets["stylesheets"].append(("less", url_for('static', filename='less/octoprint.less'))) - elif preferred_stylesheet == "css": - assets["stylesheets"].append(("css", url_for('static', filename='css/octoprint.css'))) - - asset_plugins = pluginManager.get_implementations(octoprint.plugin.AssetPlugin) - for implementation in asset_plugins: - name = implementation._identifier - all_assets = implementation.get_assets() - - if "js" in all_assets: - for asset in all_assets["js"]: - assets["js"].append(url_for('plugin_assets', name=name, filename=asset)) - - if preferred_stylesheet in all_assets: - for asset in all_assets[preferred_stylesheet]: - assets["stylesheets"].append((preferred_stylesheet, url_for('plugin_assets', name=name, filename=asset))) - else: - for stylesheet in supported_stylesheets: - if not stylesheet in all_assets: - continue - - for asset in all_assets[stylesheet]: - assets["stylesheets"].append((stylesheet, url_for('plugin_assets', name=name, filename=asset))) - break - - ##~~ prepare templates - - templates = dict( - navbar=dict(order=[], entries=dict()), - sidebar=dict(order=[], entries=dict()), - tab=dict(order=[], entries=dict()), - settings=dict(order=[], entries=dict()), - usersettings=dict(order=[], entries=dict()), - generic=dict(order=[], entries=dict()) - ) - template_types = templates.keys() - - # navbar - - templates["navbar"]["entries"] = dict( - settings=dict(template="navbar/settings.jinja2", _div="navbar_settings", styles=["display: none"], data_bind="visible: loginState.isAdmin") - ) - if enable_accesscontrol: - templates["navbar"]["entries"]["login"] = dict(template="navbar/login.jinja2", _div="navbar_login", classes=["dropdown"], custom_bindings=False) - if enable_systemmenu: - templates["navbar"]["entries"]["systemmenu"] = dict(template="navbar/systemmenu.jinja2", _div="navbar_systemmenu", styles=["display: none"], classes=["dropdown"], data_bind="visible: loginState.isAdmin", custom_bindings=False) - - # sidebar - - templates["sidebar"]["entries"]= dict( - connection=(gettext("Connection"), dict(template="sidebar/connection.jinja2", _div="connection", icon="signal", styles_wrapper=["display: none"], data_bind="visible: loginState.isAdmin")), - state=(gettext("State"), dict(template="sidebar/state.jinja2", _div="state", icon="info-sign")), - files=(gettext("Files"), dict(template="sidebar/files.jinja2", _div="files", icon="list", classes_content=["overflow_visible"], template_header="sidebar/files_header.jinja2")) - ) - - # tabs - - templates["tab"]["entries"] = dict( - temperature=(gettext("Temperature"), dict(template="tabs/temperature.jinja2", _div="temp")), - control=(gettext("Control"), dict(template="tabs/control.jinja2", _div="control")), - terminal=(gettext("Terminal"), dict(template="tabs/terminal.jinja2", _div="term")), - ) - if enable_gcodeviewer: - templates["tab"]["entries"]["gcodeviewer"] = (gettext("GCode Viewer"), dict(template="tabs/gcodeviewer.jinja2", _div="gcode")) - if enable_timelapse: - templates["tab"]["entries"]["timelapse"] = (gettext("Timelapse"), dict(template="tabs/timelapse.jinja2", _div="timelapse")) - - # settings dialog - - templates["settings"]["entries"] = dict( - section_printer=(gettext("Printer"), None), - - serial=(gettext("Serial Connection"), dict(template="dialogs/settings/serialconnection.jinja2", _div="settings_serialConnection", custom_bindings=False)), - printerprofiles=(gettext("Printer Profiles"), dict(template="dialogs/settings/printerprofiles.jinja2", _div="settings_printerProfiles", custom_bindings=False)), - temperatures=(gettext("Temperatures"), dict(template="dialogs/settings/temperatures.jinja2", _div="settings_temperature", custom_bindings=False)), - terminalfilters=(gettext("Terminal Filters"), dict(template="dialogs/settings/terminalfilters.jinja2", _div="settings_terminalFilters", custom_bindings=False)), - gcodescripts=(gettext("GCODE Scripts"), dict(template="dialogs/settings/gcodescripts.jinja2", _div="settings_gcodeScripts", custom_bindings=False)), - - section_features=(gettext("Features"), None), - - features=(gettext("Features"), dict(template="dialogs/settings/features.jinja2", _div="settings_features", custom_bindings=False)), - webcam=(gettext("Webcam"), dict(template="dialogs/settings/webcam.jinja2", _div="settings_webcam", custom_bindings=False)), - api=(gettext("API"), dict(template="dialogs/settings/api.jinja2", _div="settings_api", custom_bindings=False)), - - section_octoprint=(gettext("OctoPrint"), None), - - folders=(gettext("Folders"), dict(template="dialogs/settings/folders.jinja2", _div="settings_folders", custom_bindings=False)), - appearance=(gettext("Appearance"), dict(template="dialogs/settings/appearance.jinja2", _div="settings_appearance", custom_bindings=False)), - logs=(gettext("Logs"), dict(template="dialogs/settings/logs.jinja2", _div="settings_logs")), - ) - if enable_accesscontrol: - templates["settings"]["entries"]["accesscontrol"] = (gettext("Access Control"), dict(template="dialogs/settings/accesscontrol.jinja2", _div="settings_users", custom_bindings=False)) - - # user settings dialog - - if enable_accesscontrol: - templates["usersettings"]["entries"] = dict( - access=(gettext("Access"), dict(template="dialogs/usersettings/access.jinja2", _div="usersettings_access", custom_bindings=False)), - interface=(gettext("Interface"), dict(template="dialogs/usersettings/interface.jinja2", _div="usersettings_interface", custom_bindings=False)), - ) - - # extract data from template plugins - - template_plugins = pluginManager.get_implementations(octoprint.plugin.TemplatePlugin) - - # rules for transforming template configs to template entries - rules = dict( - navbar=dict(div=lambda x: "navbar_plugin_" + x, template=lambda x: x + "_navbar.jinja2", to_entry=lambda data: data), - sidebar=dict(div=lambda x: "sidebar_plugin_" + x, template=lambda x: x + "_sidebar.jinja2", to_entry=lambda data: (data["name"], data)), - tab=dict(div=lambda x: "tab_plugin_" + x, template=lambda x: x + "_tab.jinja2", to_entry=lambda data: (data["name"], data)), - settings=dict(div=lambda x: "settings_plugin_" + x, template=lambda x: x + "_settings.jinja2", to_entry=lambda data: (data["name"], data)), - usersettings=dict(div=lambda x: "usersettings_plugin_" + x, template=lambda x: x + "_usersettings.jinja2", to_entry=lambda data: (data["name"], data)), - generic=dict(template=lambda x: x + ".jinja2", to_entry=lambda data: data) - ) - - plugin_vars = dict() - plugin_names = set() - for implementation in template_plugins: - name = implementation._identifier - plugin_names.add(name) - - vars = implementation.get_template_vars() - if not isinstance(vars, dict): - vars = dict() - - for var_name, var_value in vars.items(): - plugin_vars["plugin_" + name + "_" + var_name] = var_value - - configs = implementation.get_template_configs() - if not isinstance(configs, (list, tuple)): - configs = [] - - includes = _process_template_configs(name, implementation, configs, rules) - - for t in template_types: - for include in includes[t]: - if t == "navbar" or t == "generic": - data = include - else: - data = include[1] - - key = data["_key"] - if "replaces" in data: - key = data["replaces"] - templates[t]["entries"][key] = include - - #~~ order internal templates and plugins - - # make sure that - # 1) we only have keys in our ordered list that we have entries for and - # 2) we have all entries located somewhere within the order - - for t in template_types: - default_order = settings().get(["appearance", "components", "order", t], merged=True, config=dict()) - configured_order = settings().get(["appearance", "components", "order", t], merged=True) - configured_disabled = settings().get(["appearance", "components", "disabled", t]) - - # first create the ordered list of all component ids according to the configured order - templates[t]["order"] = [x for x in configured_order if x in templates[t]["entries"] and not x in configured_disabled] - - # now append the entries from the default order that are not already in there - templates[t]["order"] += [x for x in default_order if not x in templates[t]["order"] and x in templates[t]["entries"] and not x in configured_disabled] - - all_ordered = set(templates[t]["order"]) - all_disabled = set(configured_disabled) - - # check if anything is missing, if not we are done here - missing_in_order = set(templates[t]["entries"].keys()).difference(all_ordered).difference(all_disabled) - if len(missing_in_order) == 0: - continue - - # finally add anything that's not included in our order yet - sorted_missing = list(missing_in_order) - if not t == "navbar" and not t == "generic": - # anything but navbar and generic components get sorted by their name - sorted_missing = sorted(missing_in_order, key=lambda x: templates[t]["entries"][x][0]) - - if t == "navbar": - # additional navbar components are prepended - templates[t]["order"] = sorted_missing + templates[t]["order"] - elif t == "sidebar" or t == "tab" or t == "generic" or t == "usersettings": - # additional sidebar, generic or usersettings components are appended - templates[t]["order"] += sorted_missing - elif t == "settings": - # additional settings items are added to the plugin section - templates[t]["entries"]["section_plugins"] = (gettext("Plugins"), None) - templates[t]["order"] += ["section_plugins"] + sorted_missing - - #~~ prepare full set of template vars for rendering - - render_kwargs = dict( - webcamStream=settings().get(["webcam", "stream"]), - enableTemperatureGraph=settings().get(["feature", "temperatureGraph"]), - enableAccessControl=userManager is not None, - enableSdSupport=settings().get(["feature", "sdSupport"]), - firstRun=settings().getBoolean(["server", "firstRun"]) and (userManager is None or not userManager.hasBeenCustomized()), - debug=debug, - version=VERSION, - display_version=DISPLAY_VERSION, - gcodeMobileThreshold=settings().get(["gcodeViewer", "mobileSizeThreshold"]), - gcodeThreshold=settings().get(["gcodeViewer", "sizeThreshold"]), - uiApiKey=UI_API_KEY, - templates=templates, - assets=assets, - pluginNames=plugin_names, - locales=locales - ) - render_kwargs.update(plugin_vars) - - #~~ render! - - return render_template( - "index.jinja2", - **render_kwargs - ) - - -def _process_template_configs(name, implementation, configs, rules): - from jinja2.exceptions import TemplateNotFound - - counters = dict( - navbar=1, - sidebar=1, - tab=1, - settings=1, - generic=1 - ) - includes = defaultdict(list) - - for config in configs: - if not isinstance(config, dict): - continue - if not "type" in config: - continue - - template_type = config["type"] - del config["type"] - - if not template_type in rules: - continue - rule = rules[template_type] - - data = _process_template_config(name, implementation, rule, config=config, counter=counters[template_type]) - if data is None: - continue - - includes[template_type].append(rule["to_entry"](data)) - counters[template_type] += 1 - - for template_type in rules: - if len(includes[template_type]) == 0: - # if no template of that type was added by the config, we'll try to use the default template name - rule = rules[template_type] - data = _process_template_config(name, implementation, rule) - if data is not None: - try: - app.jinja_env.get_or_select_template(data["template"]) - except TemplateNotFound: - pass - else: - includes[template_type].append(rule["to_entry"](data)) - - return includes - -def _process_template_config(name, implementation, rule, config=None, counter=1): - if "mandatory" in rule: - for mandatory in rule["mandatory"]: - if not mandatory in config: - return None - - if config is None: - config = dict() - data = dict(config) - - if not "suffix" in data and counter > 1: - data["suffix"] = "_%d" % counter - - if "div" in data: - data["_div"] = data["div"] - elif "div" in rule: - data["_div"] = rule["div"](name) - if "suffix" in data: - data["_div"] = data["_div"] + data["suffix"] - - if not "template" in data: - data["template"] = rule["template"](name) - - if not "name" in data: - data["name"] = implementation._plugin_name - - if not "custom_bindings" in data or data["custom_bindings"]: - data_bind = "allowBindings: true" - if "data_bind" in data: - data_bind = data_bind + ", " + data["data_bind"] - data["data_bind"] = data_bind - - data["_key"] = "plugin_" + name - if "suffix" in data: - data["_key"] += data["suffix"] - - return data - -@app.route("/robots.txt") -def robotsTxt(): - return send_from_directory(app.static_folder, "robots.txt") - - -@app.route("/i18n//.js") -@util.flask.cached(refreshif=lambda: util.flask.cache_check_headers() or "_refresh" in request.values, key=lambda: "view/%s/%s" % (request.path, g.locale)) -def localeJs(locale, domain): - from flask import _request_ctx_stack - from babel.messages.pofile import read_po - - def messages_from_po(base_path, locale, domain): - path = os.path.join(base_path, locale) - if not os.path.isdir(path): - return dict(), None - - path = os.path.join(path, "LC_MESSAGES", "{domain}.po".format(**locals())) - if not os.path.isfile(path): - return dict(), None - - messages = dict() - with file(path) as f: - catalog = read_po(f, locale=locale, domain=domain) - - for message in catalog: - message_id = message.id - if isinstance(message_id, (list, tuple)): - message_id = message_id[0] - messages[message_id] = message.string - - return messages, catalog.plural_expr - - messages = dict() - - ctx = _request_ctx_stack.top - base_path = os.path.join(ctx.app.root_path, "translations") - - plugins = octoprint.plugin.plugin_manager().enabled_plugins - for name, plugin in plugins.items(): - plugin_path = os.path.join(plugin.location, 'translations') - plugin_messages, _ = messages_from_po(plugin_path, locale, domain) - messages = octoprint.util.dict_merge(messages, plugin_messages) - - core_messages, plural_expr = messages_from_po(base_path, locale, domain) - messages = octoprint.util.dict_merge(messages, core_messages) - - catalog = dict( - messages=messages, - plural_expr=plural_expr, - locale=locale, - domain=domain - ) - - return render_template("i18n.js.jinja2", catalog=catalog) - - -@app.route("/plugin_assets//") -def plugin_assets(name, filename): - asset_plugins = pluginManager.get_filtered_implementations(lambda p: p._identifier == name, octoprint.plugin.AssetPlugin) - - if not asset_plugins: - return make_response("Asset not found", 404) - - if len(asset_plugins) > 1: - return make_response("More than one asset provider for {name}, can't proceed".format(name=name), 500) - - asset_plugin = asset_plugins[0] - asset_folder = asset_plugin.get_asset_folder() - if asset_folder is None: - return make_response("Asset not found", 404) - - return send_from_directory(asset_folder, filename) - +LOCALES = [] +LANGUAGES = set() @identity_loaded.connect_via(app) def on_identity_loaded(sender, identity): @@ -559,7 +83,6 @@ def on_identity_loaded(sender, identity): if user.is_admin(): identity.provides.add(RoleNeed("admin")) - def load_user(id): if id == "_api": return users.ApiUser() @@ -601,6 +124,9 @@ class Server(): if not self._allowRoot: self._check_for_root() + global app + global babel + global printer global printerProfileManager global fileManager @@ -621,7 +147,17 @@ class Server(): debug = self._debug # first initialize the settings singleton and make sure it uses given configfile and basedir if available - settings(init=True, basedir=self._basedir, configfile=self._configfile) + s = settings(init=True, basedir=self._basedir, configfile=self._configfile) + + # then monkey patch a bunch of stuff + util.tornado.fix_ioloop_scheduling() + util.flask.enable_additional_translations(additional_folders=[s.getBaseFolder("translations")]) + + # setup app + self._setup_app() + + # setup i18n + self._setup_i18n(app) # then initialize logging self._setup_logging(self._debug, self._logConf) @@ -637,9 +173,9 @@ class Server(): printerProfileManager = PrinterProfileManager() eventManager = events.eventManager() analysisQueue = octoprint.filemanager.analysis.AnalysisQueue() - slicingManager = octoprint.slicing.SlicingManager(settings().getBaseFolder("slicingProfiles"), printerProfileManager) + slicingManager = octoprint.slicing.SlicingManager(s.getBaseFolder("slicingProfiles"), printerProfileManager) storage_managers = dict() - storage_managers[octoprint.filemanager.FileDestinations.LOCAL] = octoprint.filemanager.storage.LocalFileStorage(settings().getBaseFolder("uploads")) + storage_managers[octoprint.filemanager.FileDestinations.LOCAL] = octoprint.filemanager.storage.LocalFileStorage(s.getBaseFolder("uploads")) fileManager = octoprint.filemanager.FileManager(analysisQueue, slicingManager, printerProfileManager, initial_storage_managers=storage_managers) printer = Printer(fileManager, analysisQueue, printerProfileManager) appSessionManager = util.flask.AppSessionManager() @@ -705,8 +241,8 @@ class Server(): if self._debug: events.DebugEventListener() - if settings().getBoolean(["accessControl", "enabled"]): - userManagerName = settings().get(["accessControl", "userManager"]) + if s.getBoolean(["accessControl", "enabled"]): + userManagerName = s.get(["accessControl", "userManager"]) try: clazz = octoprint.util.get_class(userManagerName) userManager = clazz() @@ -715,22 +251,22 @@ class Server(): app.wsgi_app = util.ReverseProxied( app.wsgi_app, - settings().get(["server", "reverseProxy", "prefixHeader"]), - settings().get(["server", "reverseProxy", "schemeHeader"]), - settings().get(["server", "reverseProxy", "hostHeader"]), - settings().get(["server", "reverseProxy", "prefixFallback"]), - settings().get(["server", "reverseProxy", "schemeFallback"]), - settings().get(["server", "reverseProxy", "hostFallback"]) + s.get(["server", "reverseProxy", "prefixHeader"]), + s.get(["server", "reverseProxy", "schemeHeader"]), + s.get(["server", "reverseProxy", "hostHeader"]), + s.get(["server", "reverseProxy", "prefixFallback"]), + s.get(["server", "reverseProxy", "schemeFallback"]), + s.get(["server", "reverseProxy", "hostFallback"]) ) - secret_key = settings().get(["server", "secretKey"]) + secret_key = s.get(["server", "secretKey"]) if not secret_key: import string from random import choice chars = string.ascii_lowercase + string.ascii_uppercase + string.digits secret_key = "".join(choice(chars) for _ in xrange(32)) - settings().set(["server", "secretKey"], secret_key) - settings().save() + s.set(["server", "secretKey"], secret_key) + s.save() app.secret_key = secret_key loginManager = LoginManager() loginManager.session_protection = "strong" @@ -741,9 +277,9 @@ class Server(): loginManager.init_app(app) if self._host is None: - self._host = settings().get(["server", "host"]) + self._host = s.get(["server", "host"]) if self._port is None: - self._port = settings().getInt(["server", "port"]) + self._port = s.getInt(["server", "port"]) app.debug = self._debug @@ -762,13 +298,13 @@ class Server(): self._router = SockJSRouter(self._create_socket_connection, "/sockjs") - upload_suffixes = dict(name=settings().get(["server", "uploads", "nameSuffix"]), path=settings().get(["server", "uploads", "pathSuffix"])) + upload_suffixes = dict(name=s.get(["server", "uploads", "nameSuffix"]), path=s.get(["server", "uploads", "pathSuffix"])) server_routes = self._router.urls + [ - (r"/downloads/timelapse/([^/]*\.mpg)", util.tornado.LargeResponseHandler, dict(path=settings().getBaseFolder("timelapse"), as_attachment=True)), - (r"/downloads/files/local/(.*)", util.tornado.LargeResponseHandler, dict(path=settings().getBaseFolder("uploads"), as_attachment=True, path_validation=util.tornado.path_validation_factory(lambda path: not os.path.basename(path).startswith("."), status_code=404))), - (r"/downloads/logs/([^/]*)", util.tornado.LargeResponseHandler, dict(path=settings().getBaseFolder("logs"), as_attachment=True, access_validation=util.tornado.access_validation_factory(app, loginManager, util.flask.admin_validator))), - (r"/downloads/camera/current", util.tornado.UrlForwardHandler, dict(url=settings().get(["webcam", "snapshot"]), as_attachment=True, access_validation=util.tornado.access_validation_factory(app, loginManager, util.flask.user_validator))), + (r"/downloads/timelapse/([^/]*\.mpg)", util.tornado.LargeResponseHandler, dict(path=s.getBaseFolder("timelapse"), as_attachment=True)), + (r"/downloads/files/local/(.*)", util.tornado.LargeResponseHandler, dict(path=s.getBaseFolder("uploads"), as_attachment=True, path_validation=util.tornado.path_validation_factory(lambda path: not os.path.basename(path).startswith("."), status_code=404))), + (r"/downloads/logs/([^/]*)", util.tornado.LargeResponseHandler, dict(path=s.getBaseFolder("logs"), as_attachment=True, access_validation=util.tornado.access_validation_factory(app, loginManager, util.flask.admin_validator))), + (r"/downloads/camera/current", util.tornado.UrlForwardHandler, dict(url=s.get(["webcam", "snapshot"]), as_attachment=True, access_validation=util.tornado.access_validation_factory(app, loginManager, util.flask.user_validator))), ] for name, hook in pluginManager.get_hooks("octoprint.server.http.routes").items(): try: @@ -795,7 +331,7 @@ class Server(): self._tornado_app = Application(server_routes) max_body_sizes = [ - ("POST", r"/api/files/([^/]*)", settings().getInt(["server", "uploads", "maxSize"])) + ("POST", r"/api/files/([^/]*)", s.getInt(["server", "uploads", "maxSize"])) ] # allow plugins to extend allowed maximum body sizes @@ -820,12 +356,12 @@ class Server(): self._logger.debug("Adding maximum body size of {size}B for {method} requests to {route})".format(**locals())) max_body_sizes.append((method, route, size)) - self._server = util.tornado.CustomHTTPServer(self._tornado_app, max_body_sizes=max_body_sizes, default_max_body_size=settings().getInt(["server", "maxSize"])) + self._server = util.tornado.CustomHTTPServer(self._tornado_app, max_body_sizes=max_body_sizes, default_max_body_size=s.getInt(["server", "maxSize"])) self._server.listen(self._port, address=self._host) eventManager.fire(events.Events.STARTUP) - if settings().getBoolean(["serial", "autoconnect"]): - (port, baudrate) = settings().get(["serial", "port"]), settings().getInt(["serial", "baudrate"]) + if s.getBoolean(["serial", "autoconnect"]): + (port, baudrate) = s.get(["serial", "port"]), s.getInt(["serial", "baudrate"]) printer_profile = printerProfileManager.get_default() connectionOptions = get_connection_options() if port in connectionOptions["ports"]: @@ -833,7 +369,7 @@ class Server(): # start up watchdogs observer = Observer() - observer.schedule(util.watchdog.GcodeWatchdogHandler(fileManager, printer), settings().getBaseFolder("watched")) + observer.schedule(util.watchdog.GcodeWatchdogHandler(fileManager, printer), s.getBaseFolder("watched")) observer.start() # run our startup plugins @@ -897,6 +433,27 @@ class Server(): if "geteuid" in dir(os) and os.geteuid() == 0: exit("You should not run OctoPrint as root!") + def _get_locale(self): + global LANGUAGES + + if "l10n" in request.values: + return Locale.negotiate([request.values["l10n"]], LANGUAGES) + + if hasattr(g, "identity") and g.identity and userManager is not None: + userid = g.identity.id + try: + user_language = userManager.getUserSetting(userid, ("interface", "language")) + if user_language is not None and not user_language == "_default": + return Locale.negotiate([user_language], LANGUAGES) + except octoprint.users.UnknownUser: + pass + + default_language = settings().get(["appearance", "defaultLanguage"]) + if default_language is not None and not default_language == "_default" and default_language in LANGUAGES: + return Locale.negotiate([default_language], LANGUAGES) + + return request.accept_languages.best_match(LANGUAGES) + def _setup_logging(self, debug, logConf=None): defaultConfig = { "version": 1, @@ -971,6 +528,45 @@ class Server(): logging.getLogger("SERIAL").setLevel(logging.DEBUG) logging.getLogger("SERIAL").debug("Enabling serial logging") + def _setup_app(self): + @app.before_request + def before_request(): + g.locale = self._get_locale() + + @app.after_request + def after_request(response): + # send no-cache headers with all POST responses + if request.method == "POST": + response.cache_control.no_cache = True + response.headers.add("X-Clacks-Overhead", "GNU Terry Pratchett") + return response + + def _setup_i18n(self, app): + global babel + global LOCALES + global LANGUAGES + + babel = Babel(app) + + def get_available_locale_identifiers(locales): + result = set() + + # add available translations + for locale in locales: + result.add(locale.language) + if locale.territory: + # if a territory is specified, add that too + result.add("%s_%s" % (locale.language, locale.territory)) + + return result + + LOCALES = babel.list_translations() + LANGUAGES = get_available_locale_identifiers(LOCALES) + + @babel.localeselector + def get_locale(): + return self._get_locale() + def _setup_jinja2(self): app.jinja_env.add_extension("jinja2.ext.do") @@ -1006,6 +602,7 @@ class Server(): def _setup_blueprints(self): from octoprint.server.api import api from octoprint.server.apps import apps + import octoprint.server.views app.register_blueprint(api, url_prefix="/api") app.register_blueprint(apps, url_prefix="/apps") diff --git a/src/octoprint/server/util/flask.py b/src/octoprint/server/util/flask.py index e1aa70e2..45ca338e 100644 --- a/src/octoprint/server/util/flask.py +++ b/src/octoprint/server/util/flask.py @@ -26,22 +26,51 @@ from werkzeug.contrib.cache import SimpleCache #~~ monkey patching -def enable_plugin_translations(): +def enable_additional_translations(default_locale="en", additional_folders=None): import os from flask import _request_ctx_stack - from babel import support + from babel import support, Locale import flask.ext.babel import octoprint.plugin + if additional_folders is None: + additional_folders = [] + + logger = logging.getLogger(__name__) + + def fixed_list_translations(self): + """Returns a list of all the locales translations exist for. The + list returned will be filled with actual locale objects and not just + strings. + """ + def list_translations(dirname): + if not os.path.isdir(dirname): + return [] + result = [] + for folder in os.listdir(dirname): + locale_dir = os.path.join(dirname, folder, 'LC_MESSAGES') + if not os.path.isdir(locale_dir): + continue + if filter(lambda x: x.endswith('.mo'), os.listdir(locale_dir)): + result.append(Locale.parse(folder)) + if not result: + result.append(Locale.parse(self._default_locale)) + return result + + dirs = additional_folders + [os.path.join(self.app.root_path, 'translations')] + + result = [Locale.parse(default_locale)] + for dir in dirs: + result += list_translations(dir) + return result + def fixed_get_translations(): """Returns the correct gettext translations that should be used for this request. This will never fail and return a dummy translation object if used outside of the request or if a translation cannot be found. """ - logger = logging.getLogger(__name__) - ctx = _request_ctx_stack.top if ctx is None: return None @@ -50,26 +79,42 @@ def enable_plugin_translations(): locale = flask.ext.babel.get_locale() translations = support.Translations() - plugins = octoprint.plugin.plugin_manager().enabled_plugins - for name, plugin in plugins.items(): - dirname = os.path.join(plugin.location, 'translations') - if not os.path.isdir(dirname): - continue + if str(locale) != default_locale: + # plugin translations + plugins = octoprint.plugin.plugin_manager().enabled_plugins + for name, plugin in plugins.items(): + dirs = map(lambda x: os.path.join(x, "_plugins", name), additional_folders) + [os.path.join(plugin.location, 'translations')] + for dirname in dirs: + if not os.path.isdir(dirname): + continue - try: - plugin_translations = support.Translations.load(dirname, [locale]) - except: - logger.exception("Error while trying to load translations for plugin {name}".format(**locals())) + try: + plugin_translations = support.Translations.load(dirname, [locale]) + except: + logger.exception("Error while trying to load translations for plugin {name}".format(**locals())) + else: + if isinstance(plugin_translations, support.Translations): + translations = translations.merge(plugin_translations) + logger.debug("Using translation folder {dirname} for locale {locale} of plugin {name}".format(**locals())) + break + else: + logger.debug("No translations for locale {locale} for plugin {name}".format(**locals())) + + # core translations + dirs = additional_folders + [os.path.join(ctx.app.root_path, 'translations')] + for dirname in dirs: + core_translations = support.Translations.load(dirname, [locale]) + if isinstance(core_translations, support.Translations): + logger.debug("Using translation folder {dirname} for locale {locale} of core translations".format(**locals())) + break else: - translations = translations.merge(plugin_translations) - - dirname = os.path.join(ctx.app.root_path, 'translations') - core_translations = support.Translations.load(dirname, [locale]) - translations = translations.merge(core_translations) + logger.debug("No core translations for locale {locale}") + translations = translations.merge(core_translations) ctx.babel_translations = translations return translations + flask.ext.babel.Babel.list_translations = fixed_list_translations flask.ext.babel.get_translations = fixed_get_translations #~~ passive login helper diff --git a/src/octoprint/server/views.py b/src/octoprint/server/views.py new file mode 100644 index 00000000..a0392488 --- /dev/null +++ b/src/octoprint/server/views.py @@ -0,0 +1,473 @@ +# coding=utf-8 +from __future__ import absolute_import + +__author__ = "Gina Häußge " +__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html' +__copyright__ = "Copyright (C) 2015 The OctoPrint Project - Released under terms of the AGPLv3 License" + +import os + +from collections import defaultdict +from flask import request, g, url_for, make_response, render_template, send_from_directory + +import octoprint.plugin + +from octoprint.server import app, userManager, pluginManager, gettext, debug, LOCALES, VERSION, DISPLAY_VERSION, UI_API_KEY +from octoprint.settings import settings + +from . import util + +import logging +_logger = logging.getLogger(__name__) + + +@app.route("/") +@util.flask.cached(refreshif=lambda: util.flask.cache_check_headers() or "_refresh" in request.values, key=lambda: "view/%s/%s" % (request.path, g.locale)) +def index(): + + #~~ a bunch of settings + + enable_gcodeviewer = settings().getBoolean(["gcodeViewer", "enabled"]) + enable_timelapse = (settings().get(["webcam", "snapshot"]) and settings().get(["webcam", "ffmpeg"])) + enable_systemmenu = settings().get(["system"]) is not None and settings().get(["system", "actions"]) is not None and len(settings().get(["system", "actions"])) > 0 + enable_accesscontrol = userManager is not None + preferred_stylesheet = settings().get(["devel", "stylesheet"]) + locales = dict((l.language, dict(language=l.language, display=l.display_name, english=l.english_name)) for l in LOCALES) + + #~~ prepare assets + + supported_stylesheets = ("css", "less") + assets = dict( + js=[], + stylesheets=[] + ) + assets["js"] = [ + url_for('static', filename='js/app/viewmodels/appearance.js'), + url_for('static', filename='js/app/viewmodels/connection.js'), + url_for('static', filename='js/app/viewmodels/control.js'), + url_for('static', filename='js/app/viewmodels/firstrun.js'), + url_for('static', filename='js/app/viewmodels/files.js'), + url_for('static', filename='js/app/viewmodels/loginstate.js'), + url_for('static', filename='js/app/viewmodels/navigation.js'), + url_for('static', filename='js/app/viewmodels/printerstate.js'), + url_for('static', filename='js/app/viewmodels/printerprofiles.js'), + url_for('static', filename='js/app/viewmodels/settings.js'), + url_for('static', filename='js/app/viewmodels/slicing.js'), + url_for('static', filename='js/app/viewmodels/temperature.js'), + url_for('static', filename='js/app/viewmodels/terminal.js'), + url_for('static', filename='js/app/viewmodels/users.js'), + url_for('static', filename='js/app/viewmodels/log.js'), + url_for('static', filename='js/app/viewmodels/usersettings.js') + ] + if enable_gcodeviewer: + assets["js"] += [ + url_for('static', filename='js/app/viewmodels/gcode.js'), + url_for('static', filename='gcodeviewer/js/ui.js'), + url_for('static', filename='gcodeviewer/js/gCodeReader.js'), + url_for('static', filename='gcodeviewer/js/renderer.js') + ] + if enable_timelapse: + assets["js"].append(url_for('static', filename='js/app/viewmodels/timelapse.js')) + + if preferred_stylesheet == "less": + assets["stylesheets"].append(("less", url_for('static', filename='less/octoprint.less'))) + elif preferred_stylesheet == "css": + assets["stylesheets"].append(("css", url_for('static', filename='css/octoprint.css'))) + + asset_plugins = pluginManager.get_implementations(octoprint.plugin.AssetPlugin) + for implementation in asset_plugins: + name = implementation._identifier + all_assets = implementation.get_assets() + + if "js" in all_assets: + for asset in all_assets["js"]: + assets["js"].append(url_for('plugin_assets', name=name, filename=asset)) + + if preferred_stylesheet in all_assets: + for asset in all_assets[preferred_stylesheet]: + assets["stylesheets"].append((preferred_stylesheet, url_for('plugin_assets', name=name, filename=asset))) + else: + for stylesheet in supported_stylesheets: + if not stylesheet in all_assets: + continue + + for asset in all_assets[stylesheet]: + assets["stylesheets"].append((stylesheet, url_for('plugin_assets', name=name, filename=asset))) + break + + ##~~ prepare templates + + templates = dict( + navbar=dict(order=[], entries=dict()), + sidebar=dict(order=[], entries=dict()), + tab=dict(order=[], entries=dict()), + settings=dict(order=[], entries=dict()), + usersettings=dict(order=[], entries=dict()), + generic=dict(order=[], entries=dict()) + ) + template_types = templates.keys() + + # navbar + + templates["navbar"]["entries"] = dict( + settings=dict(template="navbar/settings.jinja2", _div="navbar_settings", styles=["display: none"], data_bind="visible: loginState.isAdmin") + ) + if enable_accesscontrol: + templates["navbar"]["entries"]["login"] = dict(template="navbar/login.jinja2", _div="navbar_login", classes=["dropdown"], custom_bindings=False) + if enable_systemmenu: + templates["navbar"]["entries"]["systemmenu"] = dict(template="navbar/systemmenu.jinja2", _div="navbar_systemmenu", styles=["display: none"], classes=["dropdown"], data_bind="visible: loginState.isAdmin", custom_bindings=False) + + # sidebar + + templates["sidebar"]["entries"]= dict( + connection=(gettext("Connection"), dict(template="sidebar/connection.jinja2", _div="connection", icon="signal", styles_wrapper=["display: none"], data_bind="visible: loginState.isAdmin")), + state=(gettext("State"), dict(template="sidebar/state.jinja2", _div="state", icon="info-sign")), + files=(gettext("Files"), dict(template="sidebar/files.jinja2", _div="files", icon="list", classes_content=["overflow_visible"], template_header="sidebar/files_header.jinja2")) + ) + + # tabs + + templates["tab"]["entries"] = dict( + temperature=(gettext("Temperature"), dict(template="tabs/temperature.jinja2", _div="temp")), + control=(gettext("Control"), dict(template="tabs/control.jinja2", _div="control")), + terminal=(gettext("Terminal"), dict(template="tabs/terminal.jinja2", _div="term")), + ) + if enable_gcodeviewer: + templates["tab"]["entries"]["gcodeviewer"] = (gettext("GCode Viewer"), dict(template="tabs/gcodeviewer.jinja2", _div="gcode")) + if enable_timelapse: + templates["tab"]["entries"]["timelapse"] = (gettext("Timelapse"), dict(template="tabs/timelapse.jinja2", _div="timelapse")) + + # settings dialog + + templates["settings"]["entries"] = dict( + section_printer=(gettext("Printer"), None), + + serial=(gettext("Serial Connection"), dict(template="dialogs/settings/serialconnection.jinja2", _div="settings_serialConnection", custom_bindings=False)), + printerprofiles=(gettext("Printer Profiles"), dict(template="dialogs/settings/printerprofiles.jinja2", _div="settings_printerProfiles", custom_bindings=False)), + temperatures=(gettext("Temperatures"), dict(template="dialogs/settings/temperatures.jinja2", _div="settings_temperature", custom_bindings=False)), + terminalfilters=(gettext("Terminal Filters"), dict(template="dialogs/settings/terminalfilters.jinja2", _div="settings_terminalFilters", custom_bindings=False)), + gcodescripts=(gettext("GCODE Scripts"), dict(template="dialogs/settings/gcodescripts.jinja2", _div="settings_gcodeScripts", custom_bindings=False)), + + section_features=(gettext("Features"), None), + + features=(gettext("Features"), dict(template="dialogs/settings/features.jinja2", _div="settings_features", custom_bindings=False)), + webcam=(gettext("Webcam"), dict(template="dialogs/settings/webcam.jinja2", _div="settings_webcam", custom_bindings=False)), + api=(gettext("API"), dict(template="dialogs/settings/api.jinja2", _div="settings_api", custom_bindings=False)), + + section_octoprint=(gettext("OctoPrint"), None), + + folders=(gettext("Folders"), dict(template="dialogs/settings/folders.jinja2", _div="settings_folders", custom_bindings=False)), + appearance=(gettext("Appearance"), dict(template="dialogs/settings/appearance.jinja2", _div="settings_appearance", custom_bindings=False)), + logs=(gettext("Logs"), dict(template="dialogs/settings/logs.jinja2", _div="settings_logs")), + ) + if enable_accesscontrol: + templates["settings"]["entries"]["accesscontrol"] = (gettext("Access Control"), dict(template="dialogs/settings/accesscontrol.jinja2", _div="settings_users", custom_bindings=False)) + + # user settings dialog + + if enable_accesscontrol: + templates["usersettings"]["entries"] = dict( + access=(gettext("Access"), dict(template="dialogs/usersettings/access.jinja2", _div="usersettings_access", custom_bindings=False)), + interface=(gettext("Interface"), dict(template="dialogs/usersettings/interface.jinja2", _div="usersettings_interface", custom_bindings=False)), + ) + + # extract data from template plugins + + template_plugins = pluginManager.get_implementations(octoprint.plugin.TemplatePlugin) + + # rules for transforming template configs to template entries + rules = dict( + navbar=dict(div=lambda x: "navbar_plugin_" + x, template=lambda x: x + "_navbar.jinja2", to_entry=lambda data: data), + sidebar=dict(div=lambda x: "sidebar_plugin_" + x, template=lambda x: x + "_sidebar.jinja2", to_entry=lambda data: (data["name"], data)), + tab=dict(div=lambda x: "tab_plugin_" + x, template=lambda x: x + "_tab.jinja2", to_entry=lambda data: (data["name"], data)), + settings=dict(div=lambda x: "settings_plugin_" + x, template=lambda x: x + "_settings.jinja2", to_entry=lambda data: (data["name"], data)), + usersettings=dict(div=lambda x: "usersettings_plugin_" + x, template=lambda x: x + "_usersettings.jinja2", to_entry=lambda data: (data["name"], data)), + generic=dict(template=lambda x: x + ".jinja2", to_entry=lambda data: data) + ) + + plugin_vars = dict() + plugin_names = set() + for implementation in template_plugins: + name = implementation._identifier + plugin_names.add(name) + + vars = implementation.get_template_vars() + if not isinstance(vars, dict): + vars = dict() + + for var_name, var_value in vars.items(): + plugin_vars["plugin_" + name + "_" + var_name] = var_value + + configs = implementation.get_template_configs() + if not isinstance(configs, (list, tuple)): + configs = [] + + includes = _process_template_configs(name, implementation, configs, rules) + + for t in template_types: + for include in includes[t]: + if t == "navbar" or t == "generic": + data = include + else: + data = include[1] + + key = data["_key"] + if "replaces" in data: + key = data["replaces"] + templates[t]["entries"][key] = include + + #~~ order internal templates and plugins + + # make sure that + # 1) we only have keys in our ordered list that we have entries for and + # 2) we have all entries located somewhere within the order + + for t in template_types: + default_order = settings().get(["appearance", "components", "order", t], merged=True, config=dict()) + configured_order = settings().get(["appearance", "components", "order", t], merged=True) + configured_disabled = settings().get(["appearance", "components", "disabled", t]) + + # first create the ordered list of all component ids according to the configured order + templates[t]["order"] = [x for x in configured_order if x in templates[t]["entries"] and not x in configured_disabled] + + # now append the entries from the default order that are not already in there + templates[t]["order"] += [x for x in default_order if not x in templates[t]["order"] and x in templates[t]["entries"] and not x in configured_disabled] + + all_ordered = set(templates[t]["order"]) + all_disabled = set(configured_disabled) + + # check if anything is missing, if not we are done here + missing_in_order = set(templates[t]["entries"].keys()).difference(all_ordered).difference(all_disabled) + if len(missing_in_order) == 0: + continue + + # finally add anything that's not included in our order yet + sorted_missing = list(missing_in_order) + if not t == "navbar" and not t == "generic": + # anything but navbar and generic components get sorted by their name + sorted_missing = sorted(missing_in_order, key=lambda x: templates[t]["entries"][x][0]) + + if t == "navbar": + # additional navbar components are prepended + templates[t]["order"] = sorted_missing + templates[t]["order"] + elif t == "sidebar" or t == "tab" or t == "generic" or t == "usersettings": + # additional sidebar, generic or usersettings components are appended + templates[t]["order"] += sorted_missing + elif t == "settings": + # additional settings items are added to the plugin section + templates[t]["entries"]["section_plugins"] = (gettext("Plugins"), None) + templates[t]["order"] += ["section_plugins"] + sorted_missing + + #~~ prepare full set of template vars for rendering + + render_kwargs = dict( + webcamStream=settings().get(["webcam", "stream"]), + enableTemperatureGraph=settings().get(["feature", "temperatureGraph"]), + enableAccessControl=userManager is not None, + enableSdSupport=settings().get(["feature", "sdSupport"]), + firstRun=settings().getBoolean(["server", "firstRun"]) and (userManager is None or not userManager.hasBeenCustomized()), + debug=debug, + version=VERSION, + display_version=DISPLAY_VERSION, + gcodeMobileThreshold=settings().get(["gcodeViewer", "mobileSizeThreshold"]), + gcodeThreshold=settings().get(["gcodeViewer", "sizeThreshold"]), + uiApiKey=UI_API_KEY, + templates=templates, + assets=assets, + pluginNames=plugin_names, + locales=locales + ) + render_kwargs.update(plugin_vars) + + #~~ render! + + return render_template( + "index.jinja2", + **render_kwargs + ) + + +def _process_template_configs(name, implementation, configs, rules): + from jinja2.exceptions import TemplateNotFound + + counters = dict( + navbar=1, + sidebar=1, + tab=1, + settings=1, + generic=1 + ) + includes = defaultdict(list) + + for config in configs: + if not isinstance(config, dict): + continue + if not "type" in config: + continue + + template_type = config["type"] + del config["type"] + + if not template_type in rules: + continue + rule = rules[template_type] + + data = _process_template_config(name, implementation, rule, config=config, counter=counters[template_type]) + if data is None: + continue + + includes[template_type].append(rule["to_entry"](data)) + counters[template_type] += 1 + + for template_type in rules: + if len(includes[template_type]) == 0: + # if no template of that type was added by the config, we'll try to use the default template name + rule = rules[template_type] + data = _process_template_config(name, implementation, rule) + if data is not None: + try: + app.jinja_env.get_or_select_template(data["template"]) + except TemplateNotFound: + pass + else: + includes[template_type].append(rule["to_entry"](data)) + + return includes + +def _process_template_config(name, implementation, rule, config=None, counter=1): + if "mandatory" in rule: + for mandatory in rule["mandatory"]: + if not mandatory in config: + return None + + if config is None: + config = dict() + data = dict(config) + + if not "suffix" in data and counter > 1: + data["suffix"] = "_%d" % counter + + if "div" in data: + data["_div"] = data["div"] + elif "div" in rule: + data["_div"] = rule["div"](name) + if "suffix" in data: + data["_div"] = data["_div"] + data["suffix"] + + if not "template" in data: + data["template"] = rule["template"](name) + + if not "name" in data: + data["name"] = implementation._plugin_name + + if not "custom_bindings" in data or data["custom_bindings"]: + data_bind = "allowBindings: true" + if "data_bind" in data: + data_bind = data_bind + ", " + data["data_bind"] + data["data_bind"] = data_bind + + data["_key"] = "plugin_" + name + if "suffix" in data: + data["_key"] += data["suffix"] + + return data + +@app.route("/robots.txt") +def robotsTxt(): + return send_from_directory(app.static_folder, "robots.txt") + + +@app.route("/i18n//.js") +@util.flask.cached(refreshif=lambda: util.flask.cache_check_headers() or "_refresh" in request.values, key=lambda: "view/%s/%s" % (request.path, g.locale)) +def localeJs(locale, domain): + messages = dict() + plural_expr = None + + if locale != "en": + from flask import _request_ctx_stack + from babel.messages.pofile import read_po + + def messages_from_po(base_path, locale, domain): + path = os.path.join(base_path, locale) + if not os.path.isdir(path): + return None, None + + path = os.path.join(path, "LC_MESSAGES", "{domain}.po".format(**locals())) + if not os.path.isfile(path): + return None, None + + messages = dict() + with file(path) as f: + catalog = read_po(f, locale=locale, domain=domain) + + for message in catalog: + message_id = message.id + if isinstance(message_id, (list, tuple)): + message_id = message_id[0] + messages[message_id] = message.string + + return messages, catalog.plural_expr + + user_base_path = os.path.join(settings().getBaseFolder("translations")) + user_plugin_path = os.path.join(user_base_path, "_plugins") + + # plugin translations + plugins = octoprint.plugin.plugin_manager().enabled_plugins + for name, plugin in plugins.items(): + dirs = [os.path.join(user_plugin_path, name), os.path.join(plugin.location, 'translations')] + for dirname in dirs: + if not os.path.isdir(dirname): + continue + + plugin_messages, _ = messages_from_po(dirname, locale, domain) + + if plugin_messages is not None: + messages = octoprint.util.dict_merge(messages, plugin_messages) + _logger.debug("Using translation folder {dirname} for locale {locale} of plugin {name}".format(**locals())) + break + else: + _logger.debug("No translations for locale {locale} for plugin {name}".format(**locals())) + + # core translations + ctx = _request_ctx_stack.top + base_path = os.path.join(ctx.app.root_path, "translations") + + dirs = [user_base_path, base_path] + for dirname in dirs: + core_messages, plural_expr = messages_from_po(dirname, locale, domain) + + if core_messages is not None: + messages = octoprint.util.dict_merge(messages, core_messages) + _logger.debug("Using translation folder {dirname} for locale {locale} of core translations".format(**locals())) + break + else: + _logger.debug("No core translations for locale {locale}".format(**locals())) + + catalog = dict( + messages=messages, + plural_expr=plural_expr, + locale=locale, + domain=domain + ) + + return render_template("i18n.js.jinja2", catalog=catalog) + + +@app.route("/plugin_assets//") +def plugin_assets(name, filename): + asset_plugins = pluginManager.get_filtered_implementations(lambda p: p._identifier == name, octoprint.plugin.AssetPlugin) + + if not asset_plugins: + return make_response("Asset not found", 404) + + if len(asset_plugins) > 1: + return make_response("More than one asset provider for {name}, can't proceed".format(name=name), 500) + + asset_plugin = asset_plugins[0] + asset_folder = asset_plugin.get_asset_folder() + if asset_folder is None: + return make_response("Asset not found", 404) + + return send_from_directory(asset_folder, filename) + + diff --git a/src/octoprint/settings.py b/src/octoprint/settings.py index 8a4ad5c5..954b01e5 100644 --- a/src/octoprint/settings.py +++ b/src/octoprint/settings.py @@ -153,7 +153,8 @@ default_settings = { "plugins": None, "slicingProfiles": None, "printerProfiles": None, - "scripts": None + "scripts": None, + "translations": None }, "temperature": { "profiles": [