Adjusted messages.js compilation, debugged new i18n handling and added logging
This commit is contained in:
parent
baf2a77e18
commit
7f8a3849c7
4 changed files with 642 additions and 526 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 = 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,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
|
||||
|
|
|
|||
473
src/octoprint/server/views.py
Normal file
473
src/octoprint/server/views.py
Normal 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)
|
||||
|
||||
|
||||
|
|
@ -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