Allow translations to be stored in ~/.octoprint
That is the first step towards downloadable (and separately managed) translation packages, decoupled from the software's release cycle and source control. Since it will be impossible to properly maintain all languages concurrently with OctoPrint's releases, decoupling that will hopefully increase the user experience through preventing broken translations (or stalling releases due to having to wait for the translations to be updated).
This commit is contained in:
parent
baf2a77e18
commit
449dd4301e
4 changed files with 588 additions and 512 deletions
|
|
@ -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 = [Locale.parse("en")] + 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")
|
||||
|
|
|
|||
|
|
@ -26,14 +26,43 @@ from werkzeug.contrib.cache import SimpleCache
|
|||
|
||||
#~~ monkey patching
|
||||
|
||||
def enable_plugin_translations():
|
||||
def enable_additional_translations(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 = []
|
||||
|
||||
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 = [os.path.join(self.app.root_path, 'translations')] + additional_folders
|
||||
|
||||
result = []
|
||||
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
|
||||
|
|
@ -63,13 +92,17 @@ def enable_plugin_translations():
|
|||
else:
|
||||
translations = translations.merge(plugin_translations)
|
||||
|
||||
dirname = os.path.join(ctx.app.root_path, 'translations')
|
||||
core_translations = support.Translations.load(dirname, [locale])
|
||||
dirs = [os.path.join(ctx.app.root_path, 'translations')] + additional_folders
|
||||
for dirname in dirs:
|
||||
core_translations = support.Translations.load(dirname, [locale])
|
||||
if not isinstance(core_translations, support.NullTranslations):
|
||||
break
|
||||
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
|
||||
|
|
|
|||
445
src/octoprint/server/views.py
Normal file
445
src/octoprint/server/views.py
Normal file
|
|
@ -0,0 +1,445 @@
|
|||
# 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
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
|
||||
|
|
@ -153,7 +153,8 @@ default_settings = {
|
|||
"plugins": None,
|
||||
"slicingProfiles": None,
|
||||
"printerProfiles": None,
|
||||
"scripts": None
|
||||
"scripts": None,
|
||||
"translations": None
|
||||
},
|
||||
"temperature": {
|
||||
"profiles": [
|
||||
|
|
|
|||
Loading…
Reference in a new issue