Adjusted messages.js compilation, debugged new i18n handling and added logging

This commit is contained in:
Gina Häußge 2015-05-29 19:44:11 +02:00
parent baf2a77e18
commit 7f8a3849c7
4 changed files with 642 additions and 526 deletions

View file

@ -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/<string:locale>/<string:domain>.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/<string:name>/<path:filename>")
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")

View file

@ -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

View file

@ -0,0 +1,473 @@
# coding=utf-8
from __future__ import absolute_import
__author__ = "Gina Häußge <osd@foosel.net>"
__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/<string:locale>/<string:domain>.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/<string:name>/<path:filename>")
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)

View file

@ -153,7 +153,8 @@ default_settings = {
"plugins": None,
"slicingProfiles": None,
"printerProfiles": None,
"scripts": None
"scripts": None,
"translations": None
},
"temperature": {
"profiles": [