WIP on introducing user settings (incl. interface language)
This commit is contained in:
parent
7bd83e7ef3
commit
f98ebaafee
20 changed files with 457 additions and 69 deletions
|
|
@ -8,7 +8,7 @@ __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms
|
||||||
import uuid
|
import uuid
|
||||||
from sockjs.tornado import SockJSRouter
|
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, render_template, send_from_directory, g, request, make_response, session, url_for
|
||||||
from flask.ext.login import LoginManager
|
from flask.ext.login import LoginManager, current_user
|
||||||
from flask.ext.principal import Principal, Permission, RoleNeed, identity_loaded, UserNeed
|
from flask.ext.principal import Principal, Permission, RoleNeed, identity_loaded, UserNeed
|
||||||
from flask.ext.babel import Babel, gettext, ngettext
|
from flask.ext.babel import Babel, gettext, ngettext
|
||||||
from babel import Locale
|
from babel import Locale
|
||||||
|
|
@ -104,11 +104,25 @@ def after_request(response):
|
||||||
def get_locale():
|
def get_locale():
|
||||||
if "l10n" in request.values:
|
if "l10n" in request.values:
|
||||||
return Locale.negotiate([request.values["l10n"]], LANGUAGES)
|
return Locale.negotiate([request.values["l10n"]], LANGUAGES)
|
||||||
|
|
||||||
|
if g.identity:
|
||||||
|
userid = g.identity.id
|
||||||
|
try:
|
||||||
|
user_language = userManager.getUserSetting(userid, ("interface", "language"))
|
||||||
|
if user_language is not None:
|
||||||
|
return Locale.negotiate([user_language], LANGUAGES)
|
||||||
|
except octoprint.users.UnknownUser:
|
||||||
|
pass
|
||||||
|
|
||||||
|
default_language = settings().get(["appearance", "defaultLanguage"])
|
||||||
|
if default_language and default_language in LANGUAGES:
|
||||||
|
return Locale.negotiate([default_language], LANGUAGES)
|
||||||
|
|
||||||
return request.accept_languages.best_match(LANGUAGES)
|
return request.accept_languages.best_match(LANGUAGES)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
@util.flask.cached(refreshif=lambda: util.flask.cache_check_headers() or "_refresh" in request.values)
|
@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():
|
def index():
|
||||||
|
|
||||||
#~~ a bunch of settings
|
#~~ a bunch of settings
|
||||||
|
|
@ -118,8 +132,9 @@ def index():
|
||||||
enable_systemmenu = settings().get(["system"]) is not None and settings().get(["system", "actions"]) is not None and len(settings().get(["system", "actions"])) > 0
|
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
|
enable_accesscontrol = userManager is not None
|
||||||
preferred_stylesheet = settings().get(["devel", "stylesheet"])
|
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
|
#~~ prepare assets
|
||||||
|
|
||||||
supported_stylesheets = ("css", "less")
|
supported_stylesheets = ("css", "less")
|
||||||
assets = dict(
|
assets = dict(
|
||||||
|
|
@ -141,7 +156,8 @@ def index():
|
||||||
url_for('static', filename='js/app/viewmodels/temperature.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/terminal.js'),
|
||||||
url_for('static', filename='js/app/viewmodels/users.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/log.js'),
|
||||||
|
url_for('static', filename='js/app/viewmodels/usersettings.js')
|
||||||
]
|
]
|
||||||
if enable_gcodeviewer:
|
if enable_gcodeviewer:
|
||||||
assets["js"] += [
|
assets["js"] += [
|
||||||
|
|
@ -185,8 +201,10 @@ def index():
|
||||||
sidebar=dict(order=[], entries=dict()),
|
sidebar=dict(order=[], entries=dict()),
|
||||||
tab=dict(order=[], entries=dict()),
|
tab=dict(order=[], entries=dict()),
|
||||||
settings=dict(order=[], entries=dict()),
|
settings=dict(order=[], entries=dict()),
|
||||||
|
usersettings=dict(order=[], entries=dict()),
|
||||||
generic=dict(order=[], entries=dict())
|
generic=dict(order=[], entries=dict())
|
||||||
)
|
)
|
||||||
|
template_types = templates.keys()
|
||||||
|
|
||||||
# navbar
|
# navbar
|
||||||
|
|
||||||
|
|
@ -244,6 +262,14 @@ def index():
|
||||||
if enable_accesscontrol:
|
if enable_accesscontrol:
|
||||||
templates["settings"]["entries"]["accesscontrol"] = (gettext("Access Control"), dict(template="dialogs/settings/accesscontrol.jinja2", _div="settings_users", custom_bindings=False))
|
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
|
# extract data from template plugins
|
||||||
|
|
||||||
template_plugins = pluginManager.get_implementations(octoprint.plugin.TemplatePlugin)
|
template_plugins = pluginManager.get_implementations(octoprint.plugin.TemplatePlugin)
|
||||||
|
|
@ -254,6 +280,7 @@ def index():
|
||||||
sidebar=dict(div=lambda x: "sidebar_plugin_" + x, template=lambda x: x + "_sidebar.jinja2", to_entry=lambda data: (data["name"], 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)),
|
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)),
|
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)
|
generic=dict(template=lambda x: x + ".jinja2", to_entry=lambda data: data)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -273,7 +300,7 @@ def index():
|
||||||
|
|
||||||
includes = _process_template_configs(name, implementation, configs, rules)
|
includes = _process_template_configs(name, implementation, configs, rules)
|
||||||
|
|
||||||
for t in ("navbar", "sidebar", "tab", "settings", "generic"):
|
for t in template_types:
|
||||||
for include in includes[t]:
|
for include in includes[t]:
|
||||||
if t == "navbar" or t == "generic":
|
if t == "navbar" or t == "generic":
|
||||||
data = include
|
data = include
|
||||||
|
|
@ -292,7 +319,7 @@ def index():
|
||||||
# 1) we only have keys in our ordered list that we have entries for and
|
# 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
|
# 2) we have all entries located somewhere within the order
|
||||||
|
|
||||||
for t in ("navbar", "sidebar", "tab", "settings", "generic"):
|
for t in template_types:
|
||||||
configured_order = settings().get(["appearance", "components", "order", t], merged=True)
|
configured_order = settings().get(["appearance", "components", "order", t], merged=True)
|
||||||
configured_disabled = settings().get(["appearance", "components", "disabled", t])
|
configured_disabled = settings().get(["appearance", "components", "disabled", t])
|
||||||
templates[t]["order"] = [x for x in configured_order if x in templates[t]["entries"] and not x in configured_disabled]
|
templates[t]["order"] = [x for x in configured_order if x in templates[t]["entries"] and not x in configured_disabled]
|
||||||
|
|
@ -309,7 +336,7 @@ def index():
|
||||||
sorted_missing = sorted(missing_in_order, key=lambda x: templates[t]["entries"][x][0])
|
sorted_missing = sorted(missing_in_order, key=lambda x: templates[t]["entries"][x][0])
|
||||||
if t == "navbar":
|
if t == "navbar":
|
||||||
templates[t]["order"] = sorted_missing + templates[t]["order"]
|
templates[t]["order"] = sorted_missing + templates[t]["order"]
|
||||||
elif t == "sidebar" or t == "tab" or t == "generic":
|
elif t == "sidebar" or t == "tab" or t == "generic" or t == "usersettings":
|
||||||
templates[t]["order"] += sorted_missing
|
templates[t]["order"] += sorted_missing
|
||||||
elif t == "settings":
|
elif t == "settings":
|
||||||
templates[t]["entries"]["section_plugins"] = (gettext("Plugins"), None)
|
templates[t]["entries"]["section_plugins"] = (gettext("Plugins"), None)
|
||||||
|
|
@ -331,7 +358,8 @@ def index():
|
||||||
uiApiKey=UI_API_KEY,
|
uiApiKey=UI_API_KEY,
|
||||||
templates=templates,
|
templates=templates,
|
||||||
assets=assets,
|
assets=assets,
|
||||||
pluginNames=plugin_names
|
pluginNames=plugin_names,
|
||||||
|
locales=locales
|
||||||
)
|
)
|
||||||
render_kwargs.update(plugin_vars)
|
render_kwargs.update(plugin_vars)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import logging
|
||||||
import netaddr
|
import netaddr
|
||||||
import sarge
|
import sarge
|
||||||
|
|
||||||
from flask import Blueprint, request, jsonify, abort, current_app, session, make_response
|
from flask import Blueprint, request, jsonify, abort, current_app, session, make_response, g
|
||||||
from flask.ext.login import login_user, logout_user, current_user
|
from flask.ext.login import login_user, logout_user, current_user
|
||||||
from flask.ext.principal import Identity, identity_changed, AnonymousIdentity
|
from flask.ext.principal import Identity, identity_changed, AnonymousIdentity
|
||||||
|
|
||||||
|
|
@ -20,7 +20,7 @@ import octoprint.plugin
|
||||||
from octoprint.server import admin_permission, NO_CONTENT
|
from octoprint.server import admin_permission, NO_CONTENT
|
||||||
from octoprint.settings import settings as s, valid_boolean_trues
|
from octoprint.settings import settings as s, valid_boolean_trues
|
||||||
from octoprint.server.util import apiKeyRequestHandler, corsResponseHandler
|
from octoprint.server.util import apiKeyRequestHandler, corsResponseHandler
|
||||||
from octoprint.server.util.flask import restricted_access, get_remote_address, get_json_command_from_request
|
from octoprint.server.util.flask import restricted_access, get_json_command_from_request, passive_login
|
||||||
|
|
||||||
|
|
||||||
#~~ init api blueprint, including sub modules
|
#~~ init api blueprint, including sub modules
|
||||||
|
|
@ -180,40 +180,14 @@ def login():
|
||||||
if octoprint.server.userManager is not None:
|
if octoprint.server.userManager is not None:
|
||||||
user = octoprint.server.userManager.login_user(user)
|
user = octoprint.server.userManager.login_user(user)
|
||||||
session["usersession.id"] = user.get_session()
|
session["usersession.id"] = user.get_session()
|
||||||
|
g.user = user
|
||||||
login_user(user, remember=remember)
|
login_user(user, remember=remember)
|
||||||
identity_changed.send(current_app._get_current_object(), identity=Identity(user.get_id()))
|
identity_changed.send(current_app._get_current_object(), identity=Identity(user.get_id()))
|
||||||
return jsonify(user.asDict())
|
return jsonify(user.asDict())
|
||||||
return make_response(("User unknown or password incorrect", 401, []))
|
return make_response(("User unknown or password incorrect", 401, []))
|
||||||
|
|
||||||
elif "passive" in request.values.keys():
|
elif "passive" in request.values:
|
||||||
if octoprint.server.userManager is not None:
|
return passive_login()
|
||||||
user = octoprint.server.userManager.login_user(current_user)
|
|
||||||
else:
|
|
||||||
user = current_user
|
|
||||||
|
|
||||||
if user is not None and not user.is_anonymous():
|
|
||||||
identity_changed.send(current_app._get_current_object(), identity=Identity(user.get_id()))
|
|
||||||
return jsonify(user.asDict())
|
|
||||||
elif s().getBoolean(["accessControl", "autologinLocal"]) \
|
|
||||||
and s().get(["accessControl", "autologinAs"]) is not None \
|
|
||||||
and s().get(["accessControl", "localNetworks"]) is not None:
|
|
||||||
|
|
||||||
autologinAs = s().get(["accessControl", "autologinAs"])
|
|
||||||
localNetworks = netaddr.IPSet([])
|
|
||||||
for ip in s().get(["accessControl", "localNetworks"]):
|
|
||||||
localNetworks.add(ip)
|
|
||||||
|
|
||||||
try:
|
|
||||||
remoteAddr = get_remote_address(request)
|
|
||||||
if netaddr.IPAddress(remoteAddr) in localNetworks:
|
|
||||||
user = octoprint.server.userManager.findUser(autologinAs)
|
|
||||||
if user is not None:
|
|
||||||
login_user(user)
|
|
||||||
identity_changed.send(current_app._get_current_object(), identity=Identity(user.get_id()))
|
|
||||||
return jsonify(user.asDict())
|
|
||||||
except:
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
logger.exception("Could not autologin user %s for networks %r" % (autologinAs, localNetworks))
|
|
||||||
return NO_CONTENT
|
return NO_CONTENT
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,8 @@ def getSettings():
|
||||||
"appearance": {
|
"appearance": {
|
||||||
"name": s.get(["appearance", "name"]),
|
"name": s.get(["appearance", "name"]),
|
||||||
"color": s.get(["appearance", "color"]),
|
"color": s.get(["appearance", "color"]),
|
||||||
"colorTransparent": s.getBoolean(["appearance", "colorTransparent"])
|
"colorTransparent": s.getBoolean(["appearance", "colorTransparent"]),
|
||||||
|
"defaultLanguage": s.get(["appearance", "defaultLanguage"])
|
||||||
},
|
},
|
||||||
"printer": {
|
"printer": {
|
||||||
"defaultExtrusionLength": s.getInt(["printerParameters", "defaultExtrusionLength"])
|
"defaultExtrusionLength": s.getInt(["printerParameters", "defaultExtrusionLength"])
|
||||||
|
|
@ -149,6 +150,7 @@ def setSettings():
|
||||||
if "name" in data["appearance"].keys(): s.set(["appearance", "name"], data["appearance"]["name"])
|
if "name" in data["appearance"].keys(): s.set(["appearance", "name"], data["appearance"]["name"])
|
||||||
if "color" in data["appearance"].keys(): s.set(["appearance", "color"], data["appearance"]["color"])
|
if "color" in data["appearance"].keys(): s.set(["appearance", "color"], data["appearance"]["color"])
|
||||||
if "colorTransparent" in data["appearance"].keys(): s.setBoolean(["appearance", "colorTransparent"], data["appearance"]["colorTransparent"])
|
if "colorTransparent" in data["appearance"].keys(): s.setBoolean(["appearance", "colorTransparent"], data["appearance"]["colorTransparent"])
|
||||||
|
if "defaultLanguage" in data["appearance"]: s.set(["appearance", "defaultLanguage"], data["appearance"]["defaultLanguage"])
|
||||||
|
|
||||||
if "printer" in data.keys():
|
if "printer" in data.keys():
|
||||||
if "defaultExtrusionLength" in data["printer"]: s.setInt(["printerParameters", "defaultExtrusionLength"], data["printer"]["defaultExtrusionLength"])
|
if "defaultExtrusionLength" in data["printer"]: s.setInt(["printerParameters", "defaultExtrusionLength"], data["printer"]["defaultExtrusionLength"])
|
||||||
|
|
|
||||||
|
|
@ -148,6 +148,40 @@ def changePasswordForUser(username):
|
||||||
return make_response(("Forbidden", 403, []))
|
return make_response(("Forbidden", 403, []))
|
||||||
|
|
||||||
|
|
||||||
|
@api.route("/users/<username>/settings", methods=["GET"])
|
||||||
|
@restricted_access
|
||||||
|
def getSettingsForUser(username):
|
||||||
|
if userManager is None:
|
||||||
|
return jsonify(SUCCESS)
|
||||||
|
|
||||||
|
if current_user is None or current_user.is_anonymous() or (current_user.get_name() != username and not current_user.is_admin()):
|
||||||
|
return make_response("Forbidden", 403)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return jsonify(userManager.getAllUserSettings(username))
|
||||||
|
except users.UnknownUser:
|
||||||
|
return make_response("Unknown user: %s" % username, 404)
|
||||||
|
|
||||||
|
@api.route("/users/<username>/settings", methods=["PATCH"])
|
||||||
|
@restricted_access
|
||||||
|
def changeSettingsForUser(username):
|
||||||
|
if userManager is None:
|
||||||
|
return jsonify(SUCCESS)
|
||||||
|
|
||||||
|
if current_user is None or current_user.is_anonymous() or (current_user.get_name() != username and not current_user.is_admin()):
|
||||||
|
return make_response("Forbidden", 403)
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = request.json
|
||||||
|
except JSONBadRequest:
|
||||||
|
return make_response("Malformed JSON body in request", 400)
|
||||||
|
|
||||||
|
try:
|
||||||
|
userManager.changeUserSettings(username, data)
|
||||||
|
return jsonify(SUCCESS)
|
||||||
|
except users.UnknownUser:
|
||||||
|
return make_response("Unknown user: %s" % username, 404)
|
||||||
|
|
||||||
@api.route("/users/<username>/apikey", methods=["DELETE"])
|
@api.route("/users/<username>/apikey", methods=["DELETE"])
|
||||||
@restricted_access
|
@restricted_access
|
||||||
def deleteApikeyForUser(username):
|
def deleteApikeyForUser(username):
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import time
|
||||||
import uuid
|
import uuid
|
||||||
import threading
|
import threading
|
||||||
import logging
|
import logging
|
||||||
|
import netaddr
|
||||||
|
|
||||||
from octoprint.settings import settings
|
from octoprint.settings import settings
|
||||||
import octoprint.server
|
import octoprint.server
|
||||||
|
|
@ -23,11 +24,46 @@ import octoprint.users
|
||||||
from werkzeug.contrib.cache import SimpleCache
|
from werkzeug.contrib.cache import SimpleCache
|
||||||
|
|
||||||
|
|
||||||
|
#~~ passive login helper
|
||||||
|
|
||||||
|
def passive_login():
|
||||||
|
if octoprint.server.userManager is not None:
|
||||||
|
user = octoprint.server.userManager.login_user(flask.ext.login.current_user)
|
||||||
|
else:
|
||||||
|
user = flask.ext.login.current_user
|
||||||
|
|
||||||
|
if user is not None and not user.is_anonymous():
|
||||||
|
flask.g.user = user
|
||||||
|
flask.ext.principal.identity_changed.send(flask.current_app._get_current_object(), identity=flask.ext.principal.Identity(user.get_id()))
|
||||||
|
return flask.jsonify(user.asDict())
|
||||||
|
elif settings().getBoolean(["accessControl", "autologinLocal"]) \
|
||||||
|
and settings().get(["accessControl", "autologinAs"]) is not None \
|
||||||
|
and settings().get(["accessControl", "localNetworks"]) is not None:
|
||||||
|
|
||||||
|
autologinAs = settings().get(["accessControl", "autologinAs"])
|
||||||
|
localNetworks = netaddr.IPSet([])
|
||||||
|
for ip in settings().get(["accessControl", "localNetworks"]):
|
||||||
|
localNetworks.add(ip)
|
||||||
|
|
||||||
|
try:
|
||||||
|
remoteAddr = get_remote_address(flask.request)
|
||||||
|
if netaddr.IPAddress(remoteAddr) in localNetworks:
|
||||||
|
user = octoprint.server.userManager.findUser(autologinAs)
|
||||||
|
if user is not None:
|
||||||
|
flask.g.user = user
|
||||||
|
flask.ext.login.login_user(user)
|
||||||
|
flask.ext.principal.identity_changed.send(flask.current_app._get_current_object(), identity=flask.ext.principal.Identity(user.get_id()))
|
||||||
|
return flask.jsonify(user.asDict())
|
||||||
|
except:
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
logger.exception("Could not autologin user %s for networks %r" % (autologinAs, localNetworks))
|
||||||
|
|
||||||
|
|
||||||
#~~ cache decorator for cacheable views
|
#~~ cache decorator for cacheable views
|
||||||
|
|
||||||
_cache = SimpleCache()
|
_cache = SimpleCache()
|
||||||
|
|
||||||
def cached(timeout=5 * 60, key="view/%s", unless=None, refreshif=None):
|
def cached(timeout=5 * 60, key=lambda: "view/%s" % flask.request.path, unless=None, refreshif=None):
|
||||||
def decorator(f):
|
def decorator(f):
|
||||||
@functools.wraps(f)
|
@functools.wraps(f)
|
||||||
def decorated_function(*args, **kwargs):
|
def decorated_function(*args, **kwargs):
|
||||||
|
|
@ -38,7 +74,7 @@ def cached(timeout=5 * 60, key="view/%s", unless=None, refreshif=None):
|
||||||
logger.debug("Cache bypassed, calling wrapped function")
|
logger.debug("Cache bypassed, calling wrapped function")
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
cache_key = key % flask.request.path
|
cache_key = key()
|
||||||
|
|
||||||
# only take the value from the cache if we are not required to refresh it from the wrapped function
|
# only take the value from the cache if we are not required to refresh it from the wrapped function
|
||||||
if not callable(refreshif) or not refreshif():
|
if not callable(refreshif) or not refreshif():
|
||||||
|
|
|
||||||
|
|
@ -168,6 +168,7 @@ default_settings = {
|
||||||
"name": "",
|
"name": "",
|
||||||
"color": "default",
|
"color": "default",
|
||||||
"colorTransparent": False,
|
"colorTransparent": False,
|
||||||
|
"defaultLanguage": None,
|
||||||
"components": {
|
"components": {
|
||||||
"order": {
|
"order": {
|
||||||
"navbar": ["settings", "systemmenu", "login"],
|
"navbar": ["settings", "systemmenu", "login"],
|
||||||
|
|
@ -178,6 +179,7 @@ default_settings = {
|
||||||
"section_features", "features", "webcam", "accesscontrol", "api",
|
"section_features", "features", "webcam", "accesscontrol", "api",
|
||||||
"section_octoprint", "folders", "appearance", "logs"
|
"section_octoprint", "folders", "appearance", "logs"
|
||||||
],
|
],
|
||||||
|
"usersettings": ["access", "interface"],
|
||||||
"generic": []
|
"generic": []
|
||||||
},
|
},
|
||||||
"disabled": {
|
"disabled": {
|
||||||
|
|
@ -185,6 +187,7 @@ default_settings = {
|
||||||
"sidebar": [],
|
"sidebar": [],
|
||||||
"tab": [],
|
"tab": [],
|
||||||
"settings": [],
|
"settings": [],
|
||||||
|
"usersettings": [],
|
||||||
"generic": []
|
"generic": []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ $(function() {
|
||||||
self.loginState = parameters[0];
|
self.loginState = parameters[0];
|
||||||
self.appearance = parameters[1];
|
self.appearance = parameters[1];
|
||||||
self.settings = parameters[2];
|
self.settings = parameters[2];
|
||||||
self.users = parameters[3];
|
self.usersettings = parameters[3];
|
||||||
|
|
||||||
self.systemActions = self.settings.system_actions;
|
self.systemActions = self.settings.system_actions;
|
||||||
|
|
||||||
|
|
@ -54,9 +54,7 @@ $(function() {
|
||||||
|
|
||||||
OCTOPRINT_VIEWMODELS.push([
|
OCTOPRINT_VIEWMODELS.push([
|
||||||
NavigationViewModel,
|
NavigationViewModel,
|
||||||
["loginStateViewModel", "appearanceViewModel", "settingsViewModel", "usersViewModel"],
|
["loginStateViewModel", "appearanceViewModel", "settingsViewModel", "userSettingsViewModel"],
|
||||||
"#navbar"
|
"#navbar"
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// TODO usersViewModel is needed for what?
|
|
||||||
});
|
});
|
||||||
|
|
@ -13,6 +13,7 @@ $(function() {
|
||||||
self.appearance_name = ko.observable(undefined);
|
self.appearance_name = ko.observable(undefined);
|
||||||
self.appearance_color = ko.observable(undefined);
|
self.appearance_color = ko.observable(undefined);
|
||||||
self.appearance_colorTransparent = ko.observable();
|
self.appearance_colorTransparent = ko.observable();
|
||||||
|
self.appearance_defaultLanguage = ko.observable();
|
||||||
|
|
||||||
self.settingsDialog = undefined;
|
self.settingsDialog = undefined;
|
||||||
|
|
||||||
|
|
@ -53,6 +54,12 @@ $(function() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var auto_locale = {language: undefined, display: gettext("Autodetect from browser"), english: undefined};
|
||||||
|
self.locales = ko.observableArray([auto_locale].concat(_.sortBy(_.values(AVAILABLE_LOCALES), function(n) {
|
||||||
|
return n.display;
|
||||||
|
})));
|
||||||
|
self.locale_languages = _.keys(AVAILABLE_LOCALES);
|
||||||
|
|
||||||
self.printer_defaultExtrusionLength = ko.observable(undefined);
|
self.printer_defaultExtrusionLength = ko.observable(undefined);
|
||||||
|
|
||||||
self.webcam_streamUrl = ko.observable(undefined);
|
self.webcam_streamUrl = ko.observable(undefined);
|
||||||
|
|
@ -187,6 +194,11 @@ $(function() {
|
||||||
self.appearance_name(response.appearance.name);
|
self.appearance_name(response.appearance.name);
|
||||||
self.appearance_color(response.appearance.color);
|
self.appearance_color(response.appearance.color);
|
||||||
self.appearance_colorTransparent(response.appearance.colorTransparent);
|
self.appearance_colorTransparent(response.appearance.colorTransparent);
|
||||||
|
if (_.includes(self.locale_languages, response.appearance.defaultLanguage)) {
|
||||||
|
self.appearance_defaultLanguage(response.appearance.defaultLanguage);
|
||||||
|
} else {
|
||||||
|
self.appearance_defaultLanguage(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
self.printer_defaultExtrusionLength(response.printer.defaultExtrusionLength);
|
self.printer_defaultExtrusionLength(response.printer.defaultExtrusionLength);
|
||||||
|
|
||||||
|
|
@ -255,7 +267,8 @@ $(function() {
|
||||||
"appearance" : {
|
"appearance" : {
|
||||||
"name": self.appearance_name(),
|
"name": self.appearance_name(),
|
||||||
"color": self.appearance_color(),
|
"color": self.appearance_color(),
|
||||||
"colorTransparent": self.appearance_colorTransparent()
|
"colorTransparent": self.appearance_colorTransparent(),
|
||||||
|
"defaultLanguage": self.appearance_defaultLanguage()
|
||||||
},
|
},
|
||||||
"printer": {
|
"printer": {
|
||||||
"defaultExtrusionLength": self.printer_defaultExtrusionLength()
|
"defaultExtrusionLength": self.printer_defaultExtrusionLength()
|
||||||
|
|
@ -326,7 +339,7 @@ $(function() {
|
||||||
data: JSON.stringify(data),
|
data: JSON.stringify(data),
|
||||||
success: function(response) {
|
success: function(response) {
|
||||||
self.fromResponse(response);
|
self.fromResponse(response);
|
||||||
$("#settings_dialog").modal("hide");
|
self.settingsDialog.modal("hide");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,10 @@ $(function() {
|
||||||
self.editorAdmin = ko.observable(undefined);
|
self.editorAdmin = ko.observable(undefined);
|
||||||
self.editorActive = ko.observable(undefined);
|
self.editorActive = ko.observable(undefined);
|
||||||
|
|
||||||
|
self.addUserDialog = undefined;
|
||||||
|
self.editUserDialog = undefined;
|
||||||
|
self.changePasswordDialog = undefined;
|
||||||
|
|
||||||
self.currentUser.subscribe(function(newValue) {
|
self.currentUser.subscribe(function(newValue) {
|
||||||
if (newValue === undefined) {
|
if (newValue === undefined) {
|
||||||
self.editorUsername(undefined);
|
self.editorUsername(undefined);
|
||||||
|
|
@ -79,7 +83,7 @@ $(function() {
|
||||||
|
|
||||||
self.currentUser(undefined);
|
self.currentUser(undefined);
|
||||||
self.editorActive(true);
|
self.editorActive(true);
|
||||||
$("#settings-usersDialogAddUser").modal("show");
|
self.addUserDialog.modal("show");
|
||||||
};
|
};
|
||||||
|
|
||||||
self.confirmAddUser = function() {
|
self.confirmAddUser = function() {
|
||||||
|
|
@ -89,7 +93,7 @@ $(function() {
|
||||||
self.addUser(user, function() {
|
self.addUser(user, function() {
|
||||||
// close dialog
|
// close dialog
|
||||||
self.currentUser(undefined);
|
self.currentUser(undefined);
|
||||||
$("#settings-usersDialogAddUser").modal("hide");
|
self.addUserDialog.modal("hide");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -97,7 +101,7 @@ $(function() {
|
||||||
if (!CONFIG_ACCESS_CONTROL) return;
|
if (!CONFIG_ACCESS_CONTROL) return;
|
||||||
|
|
||||||
self.currentUser(user);
|
self.currentUser(user);
|
||||||
$("#settings-usersDialogEditUser").modal("show");
|
self.editUserDialog.modal("show");
|
||||||
};
|
};
|
||||||
|
|
||||||
self.confirmEditUser = function() {
|
self.confirmEditUser = function() {
|
||||||
|
|
@ -111,7 +115,7 @@ $(function() {
|
||||||
self.updateUser(user, function() {
|
self.updateUser(user, function() {
|
||||||
// close dialog
|
// close dialog
|
||||||
self.currentUser(undefined);
|
self.currentUser(undefined);
|
||||||
$("#settings-usersDialogEditUser").modal("hide");
|
self.editUserDialog.modal("hide");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -119,7 +123,7 @@ $(function() {
|
||||||
if (!CONFIG_ACCESS_CONTROL) return;
|
if (!CONFIG_ACCESS_CONTROL) return;
|
||||||
|
|
||||||
self.currentUser(user);
|
self.currentUser(user);
|
||||||
$("#settings-usersDialogChangePassword").modal("show");
|
self.changePasswordDialog.modal("show");
|
||||||
};
|
};
|
||||||
|
|
||||||
self.confirmChangePassword = function() {
|
self.confirmChangePassword = function() {
|
||||||
|
|
@ -128,7 +132,7 @@ $(function() {
|
||||||
self.updatePassword(self.currentUser().name, self.editorPassword(), function() {
|
self.updatePassword(self.currentUser().name, self.editorPassword(), function() {
|
||||||
// close dialog
|
// close dialog
|
||||||
self.currentUser(undefined);
|
self.currentUser(undefined);
|
||||||
$("#settings-usersDialogChangePassword").modal("hide");
|
self.changePasswordDialog.modal("hide");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -153,6 +157,14 @@ $(function() {
|
||||||
self.requestData();
|
self.requestData();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//~~ Framework
|
||||||
|
|
||||||
|
self.onStartup = function() {
|
||||||
|
self.addUserDialog = $("#settings-usersDialogAddUser");
|
||||||
|
self.editUserDialog = $("#settings-usersDialogEditUser");
|
||||||
|
self.changePasswordDialog = $("#settings-usersDialogChangePassword");
|
||||||
|
};
|
||||||
|
|
||||||
//~~ AJAX calls
|
//~~ AJAX calls
|
||||||
|
|
||||||
self.addUser = function(user, callback) {
|
self.addUser = function(user, callback) {
|
||||||
|
|
|
||||||
115
src/octoprint/static/js/app/viewmodels/usersettings.js
Normal file
115
src/octoprint/static/js/app/viewmodels/usersettings.js
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
$(function() {
|
||||||
|
function UserSettingsViewModel(parameters) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
self.loginState = parameters[0];
|
||||||
|
self.users = parameters[1];
|
||||||
|
|
||||||
|
self.userSettingsDialog = undefined;
|
||||||
|
|
||||||
|
var auto_locale = {language: undefined, display: gettext("Site default"), english: undefined};
|
||||||
|
self.locales = ko.observableArray([auto_locale].concat(_.sortBy(_.values(AVAILABLE_LOCALES), function(n) {
|
||||||
|
return n.display;
|
||||||
|
})));
|
||||||
|
self.locale_languages = _.keys(AVAILABLE_LOCALES);
|
||||||
|
|
||||||
|
self.access_password = ko.observable(undefined);
|
||||||
|
self.access_repeatedPassword = ko.observable(undefined);
|
||||||
|
self.access_apikey = ko.observable(undefined);
|
||||||
|
self.interface_language = ko.observable(undefined);
|
||||||
|
|
||||||
|
self.currentUser = ko.observable(undefined);
|
||||||
|
self.currentUser.subscribe(function(newUser) {
|
||||||
|
if (newUser == undefined) {
|
||||||
|
self.access_password(undefined);
|
||||||
|
self.access_repeatedPassword(undefined);
|
||||||
|
self.access_apikey(undefined);
|
||||||
|
self.interface_language(LOCALE);
|
||||||
|
} else {
|
||||||
|
self.access_apikey(newUser.apikey);
|
||||||
|
self.interface_language(undefined);
|
||||||
|
if (newUser.settings.hasOwnProperty("interface") && newUser.settings.interface.hasOwnProperty("language")) {
|
||||||
|
self.interface_language(newUser.settings.interface.language);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.passwordMismatch = ko.computed(function() {
|
||||||
|
return self.access_password() != self.access_repeatedPassword();
|
||||||
|
});
|
||||||
|
|
||||||
|
self.show = function(user) {
|
||||||
|
if (!CONFIG_ACCESS_CONTROL) return;
|
||||||
|
|
||||||
|
if (user == undefined) {
|
||||||
|
user = self.loginState.currentUser();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.currentUser(user);
|
||||||
|
self.userSettingsDialog.modal("show");
|
||||||
|
};
|
||||||
|
|
||||||
|
self.save = function() {
|
||||||
|
if (!CONFIG_ACCESS_CONTROL) return;
|
||||||
|
|
||||||
|
if (self.access_password() && !self.passwordMismatch()) {
|
||||||
|
self.users.updatePassword(self.currentUser().name, self.access_password(), function(){});
|
||||||
|
}
|
||||||
|
|
||||||
|
var settings = {
|
||||||
|
"interface": {
|
||||||
|
"language": self.interface_language()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.updateSettings(self.currentUser().name, settings, function() {
|
||||||
|
// close dialog
|
||||||
|
self.currentUser(undefined);
|
||||||
|
self.userSettingsDialog.modal("hide");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
self.updateSettings = function(username, settings, callback) {
|
||||||
|
if (!CONFIG_ACCESS_CONTROL) return;
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: API_BASEURL + "users/" + username + "/settings",
|
||||||
|
type: "PATCH",
|
||||||
|
contentType: "application/json; charset=UTF-8",
|
||||||
|
data: JSON.stringify(settings),
|
||||||
|
success: callback
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
self.saveEnabled = function() {
|
||||||
|
return !self.passwordMismatch();
|
||||||
|
};
|
||||||
|
|
||||||
|
self.onStartup = function() {
|
||||||
|
self.userSettingsDialog = $("#usersettings_dialog");
|
||||||
|
};
|
||||||
|
|
||||||
|
self.onAllBound = function(allViewModels) {
|
||||||
|
self.userSettingsDialog.on('show', function() {
|
||||||
|
_.each(allViewModels, function(viewModel) {
|
||||||
|
if (viewModel.hasOwnProperty("onUserSettingsShown")) {
|
||||||
|
viewModel.onUserSettingsShown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
self.userSettingsDialog.on('hidden', function() {
|
||||||
|
_.each(allViewModels, function(viewModel) {
|
||||||
|
if (viewModel.hasOwnProperty("onUserSettingsHidden")) {
|
||||||
|
viewModel.onUserSettingsHidden();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
OCTOPRINT_VIEWMODELS.push([
|
||||||
|
UserSettingsViewModel,
|
||||||
|
["loginStateViewModel", "usersViewModel"],
|
||||||
|
["#usersettings_dialog"]
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
@ -107,7 +107,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button class="btn" data-dismiss="modal" aria-hidden="true">{{ _('Abort') }}</button>
|
<button class="btn" data-dismiss="modal" aria-hidden="true">{{ _('Abort') }}</button>
|
||||||
<button class="btn btn-primary" data-bind="click: function() { $root.users.confirmEditUser(); }">{{ _('Confirm') }}</button>
|
<button class="btn btn-primary" data-bind="click: function() { $root.users.confirmUserSettingsDialog(); }">{{ _('Confirm') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -144,14 +144,6 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- TODO Figure out issue where text isn't properly bound and hence the QR code is worthless
|
|
||||||
<div class="control-group">
|
|
||||||
<label class="control-label">{{ _('QR Code') }}</label>
|
|
||||||
<div class="controls">
|
|
||||||
<div data-bind="qrcode: {text: $root.users.editorApiKey, size: 150}"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
-->
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -19,4 +19,15 @@
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="control-group" title="">
|
||||||
|
<label class="control-label" for="settings-appearanceDefaultLanguage">{{ _('Default Language') }}</label>
|
||||||
|
<div class="controls">
|
||||||
|
<select id="settings-appearanceDefaultLanguage" data-bind="options: locales,
|
||||||
|
optionsText: function(item) { return item.display + ((item.language != undefined && item.english != undefined) ? ' [' + item.language + ', ' + item.english + ']' : '') },
|
||||||
|
optionsValue: 'language',
|
||||||
|
value: appearance_defaultLanguage">
|
||||||
|
</select>
|
||||||
|
<span class="help-inline">{{ _('Changes to the default interface language will only become active after a reload of the page and only be active if not overridden by the user''s language settings.') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
49
src/octoprint/templates/dialogs/usersettings.jinja2
Normal file
49
src/octoprint/templates/dialogs/usersettings.jinja2
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
<div id="usersettings_dialog" class="modal hide fade">
|
||||||
|
<div class="modal-header">
|
||||||
|
<a href="#" class="close" data-dismiss="modal" aria-hidden="true">×</a>
|
||||||
|
<h3>{{ _('User Settings') }}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<ul class="nav nav-pills">
|
||||||
|
{% set mark_active = True %}
|
||||||
|
{% for key in templates.usersettings.order %}
|
||||||
|
{% set entry, data = templates.usersettings.entries[key] %}
|
||||||
|
{% if data is none %}
|
||||||
|
<li class="nav-header">{{ entry }}</li>
|
||||||
|
{% else %}
|
||||||
|
{% if "custom_bindings" not in data or data["custom_bindings"] %}<!-- ko allowBindings: false -->{% endif %}
|
||||||
|
<li id="{{ data._div }}_link"
|
||||||
|
{% if "data_bind" in data %}data-bind="{{ data.data_bind }}"{% endif %}
|
||||||
|
class="{% if mark_active %}active{% set mark_active = False %}{% endif %} {% if "classes_link" in data %}{{ data.classes_link|join(' ') }}{% elif "classes" in data %}{{ data.classes|join(' ') }}{% endif %}"
|
||||||
|
{% if "styles_link" in data %} style="{{ data.styles_link|join(', ') }}" {% elif "styles" in data %} style="{{ data.styles|join(', ') }}" {% endif %}
|
||||||
|
>
|
||||||
|
<a href="#{{ data._div }}" data-toggle="tab">{{ entry }}</a>
|
||||||
|
</li>
|
||||||
|
{% if "custom_bindings" not in data or data["custom_bindings"] %}<!-- /ko -->{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="tab-content">
|
||||||
|
{% set mark_active = True %}
|
||||||
|
{% for key in templates.usersettings.order %}
|
||||||
|
{% set entry, data = templates.usersettings.entries[key] %}
|
||||||
|
{% if data is not none %}
|
||||||
|
{% if "custom_bindings" not in data or data["custom_bindings"] %}<!-- ko allowBindings: false -->{% endif %}
|
||||||
|
<div id="{{ data._div }}"
|
||||||
|
{% if "data_bind" in data %}data-bind="{{ data.data_bind }}"{% endif %}
|
||||||
|
class="tab-pane {% if mark_active %}active{% set mark_active = False %}{% endif %} {% if classes_content in data %}{{ data.classes_content|join(' ') }}{% elif classes in data %}{{ data.classes|join(' ') }}{% endif %}"
|
||||||
|
{% if "styles_content" in data %} style="{{ data.styles_content|join(', ') }}" {% elif styles in data %} style="{{ data.styles|join(', ') }}" {% endif %}
|
||||||
|
>
|
||||||
|
{% include data.template ignore missing %}
|
||||||
|
</div>
|
||||||
|
{% if "custom_bindings" not in data or data["custom_bindings"] %}<!-- /ko -->{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn" data-dismiss="modal" aria-hidden="true">{{ _('Abort') }}</button>
|
||||||
|
<button class="btn btn-primary" data-bind="click: function() { save(); }, enable: saveEnabled()">{{ _('Confirm') }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
40
src/octoprint/templates/dialogs/usersettings/access.jinja2
Normal file
40
src/octoprint/templates/dialogs/usersettings/access.jinja2
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
<form class="form-horizontal">
|
||||||
|
<fieldset>
|
||||||
|
<legend>{{ _('Password') }}</legend>
|
||||||
|
<div class="control-group">
|
||||||
|
<label class="control-label" for="userSettings-access_password">{{ _('New Password') }}</label>
|
||||||
|
<div class="controls">
|
||||||
|
<input type="password" class="input-block-level" id="userSettings-access_password" data-bind="value: access_password" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="control-group" data-bind="css: {error: passwordMismatch()}">
|
||||||
|
<label class="control-label" for="userSettings-access_repeatedPassword">{{ _('Repeat Password') }}</label>
|
||||||
|
<div class="controls">
|
||||||
|
<input type="password" class="input-block-level" id="userSettings-access_repeatedPassword" data-bind="value: access_repeatedPassword, valueUpdate: 'afterkeydown'" required>
|
||||||
|
<span class="help-inline" data-bind="visible: passwordMismatch()">{{ _('Passwords do not match') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<legend>{{ _('API Key') }}</legend>
|
||||||
|
<div class="control-group">
|
||||||
|
<label class="control-label" for="userSettings-access_apikey">{{ _('Current API Key') }}</label>
|
||||||
|
<div class="controls">
|
||||||
|
<div class="input-append">
|
||||||
|
<input type="text" class="input-block-level uneditable-input" id="userSettings-access_apikey" data-bind="value: access_apikey, valueUpdate: 'input', attr: {placeholder: '{{ _('N/A') }}'}">
|
||||||
|
<a class="btn" title="Generate new Apikey" data-bind="click: function() { users.confirmGenerateApikey(); }"><i class="icon-refresh"></i></a>
|
||||||
|
<a class="btn btn-danger" title="Delete Apikey" data-bind="click: function() { users.confirmDeleteApikey(); }"><i class="icon-trash"></i></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- TODO Figure out issue where text isn't properly bound and hence the QR code is worthless -->
|
||||||
|
<div class="control-group">
|
||||||
|
<label class="control-label">{{ _('QR Code') }}</label>
|
||||||
|
<div class="controls">
|
||||||
|
<div data-bind="qrcode: {text: access_apikey, size: 150}"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- -->
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
<form class="form-horizontal">
|
||||||
|
<fieldset>
|
||||||
|
<legend>{{ _('Language') }}</legend>
|
||||||
|
<div class="control-group">
|
||||||
|
<div class="controls">
|
||||||
|
<select data-bind="options: locales,
|
||||||
|
optionsText: function(item) { return item.display + ((item.language != undefined && item.english != undefined) ? ' [' + item.language + ', ' + item.english + ']' : '') },
|
||||||
|
optionsValue: 'language',
|
||||||
|
value: interface_language">
|
||||||
|
</select>
|
||||||
|
<span class="help-inline">{{ _('Changes to the interface language will only become active after a reload of the page.') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
|
@ -123,6 +123,7 @@
|
||||||
{% include 'dialogs/firstrun.jinja2' %}
|
{% include 'dialogs/firstrun.jinja2' %}
|
||||||
{% include 'dialogs/settings.jinja2' %}
|
{% include 'dialogs/settings.jinja2' %}
|
||||||
{% include 'dialogs/slicing.jinja2' %}
|
{% include 'dialogs/slicing.jinja2' %}
|
||||||
|
{% include 'dialogs/usersettings.jinja2' %}
|
||||||
<!-- End of dialogs -->
|
<!-- End of dialogs -->
|
||||||
|
|
||||||
<!-- Overlays -->
|
<!-- Overlays -->
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@
|
||||||
var VERSION = "{{ version }}";
|
var VERSION = "{{ version }}";
|
||||||
var DISPLAY_VERSION = "{{ display_version }}";
|
var DISPLAY_VERSION = "{{ display_version }}";
|
||||||
var LOCALE = "{{ g.locale }}";
|
var LOCALE = "{{ g.locale }}";
|
||||||
|
var AVAILABLE_LOCALES = {{ locales|tojson }};
|
||||||
|
|
||||||
var OCTOPRINT_VIEWMODELS = [];
|
var OCTOPRINT_VIEWMODELS = [];
|
||||||
var ADDITIONAL_VIEWMODELS = [];
|
var ADDITIONAL_VIEWMODELS = [];
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,6 @@
|
||||||
<button class="btn btn-block btn-primary" id="login_button" data-bind="click: loginState.login">{{ _('Login') }}</button>
|
<button class="btn btn-block btn-primary" id="login_button" data-bind="click: loginState.login">{{ _('Login') }}</button>
|
||||||
</div>
|
</div>
|
||||||
<ul id="login_dropdown_loggedin" class="hide" data-bind="css: {hide: !loginState.loggedIn(), 'dropdown-menu': loginState.loggedIn()}">
|
<ul id="login_dropdown_loggedin" class="hide" data-bind="css: {hide: !loginState.loggedIn(), 'dropdown-menu': loginState.loggedIn()}">
|
||||||
<li><a href="#" id="change_password_button" data-bind="click: function() { users.showChangePasswordDialog(loginState.currentUser()); }">{{ _('Change Password') }}</a></li>
|
<li><a href="#" id="usersettings_button" data-bind="click: function() { usersettings.show(); }">{{ _('User Settings') }}</a></li>
|
||||||
<li><a href="#" id="logout_button" data-bind="click: loginState.logout">{{ _('Logout') }}</a></li>
|
<li><a href="#" id="logout_button" data-bind="click: loginState.logout">{{ _('Logout') }}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
||||||
|
|
@ -127,9 +127,15 @@ class UserManager(object):
|
||||||
def getUserSetting(self, username, key):
|
def getUserSetting(self, username, key):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def getAllUserSettings(self, username):
|
||||||
|
return dict()
|
||||||
|
|
||||||
def changeUserSetting(self, username, key, value):
|
def changeUserSetting(self, username, key, value):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def changeUserSettings(self, username, new_settings):
|
||||||
|
pass
|
||||||
|
|
||||||
def removeUser(self, username):
|
def removeUser(self, username):
|
||||||
if username in self._session_users_by_username:
|
if username in self._session_users_by_username:
|
||||||
users = self._session_users_by_username[username]
|
users = self._session_users_by_username[username]
|
||||||
|
|
@ -181,7 +187,10 @@ class FilebasedUserManager(UserManager):
|
||||||
apikey = None
|
apikey = None
|
||||||
if "apikey" in attributes:
|
if "apikey" in attributes:
|
||||||
apikey = attributes["apikey"]
|
apikey = attributes["apikey"]
|
||||||
self._users[name] = User(name, attributes["password"], attributes["active"], attributes["roles"], apikey)
|
settings = dict()
|
||||||
|
if "settings" in attributes:
|
||||||
|
settings = attributes["settings"]
|
||||||
|
self._users[name] = User(name, attributes["password"], attributes["active"], attributes["roles"], apikey=apikey, settings=settings)
|
||||||
else:
|
else:
|
||||||
self._customized = False
|
self._customized = False
|
||||||
|
|
||||||
|
|
@ -212,7 +221,7 @@ class FilebasedUserManager(UserManager):
|
||||||
if username in self._users.keys():
|
if username in self._users.keys():
|
||||||
raise UserAlreadyExists(username)
|
raise UserAlreadyExists(username)
|
||||||
|
|
||||||
self._users[username] = User(username, UserManager.createPasswordHash(password), active, roles, apikey)
|
self._users[username] = User(username, UserManager.createPasswordHash(password), active, roles, apikey=apikey)
|
||||||
self._dirty = True
|
self._dirty = True
|
||||||
self._save()
|
self._save()
|
||||||
|
|
||||||
|
|
@ -277,10 +286,29 @@ class FilebasedUserManager(UserManager):
|
||||||
user = self._users[username]
|
user = self._users[username]
|
||||||
current = user.get_setting(key)
|
current = user.get_setting(key)
|
||||||
if not current or current != value:
|
if not current or current != value:
|
||||||
|
old_value = user.get_setting(key)
|
||||||
user.set_setting(key, value)
|
user.set_setting(key, value)
|
||||||
self._dirty = True
|
self._dirty = self._dirty or old_value != value
|
||||||
self._save()
|
self._save()
|
||||||
|
|
||||||
|
def changeUserSettings(self, username, new_settings):
|
||||||
|
if not username in self._users:
|
||||||
|
raise UnknownUser(username)
|
||||||
|
|
||||||
|
user = self._users[username]
|
||||||
|
for key, value in new_settings.items():
|
||||||
|
old_value = user.get_setting(key)
|
||||||
|
user.set_setting(key, value)
|
||||||
|
self._dirty = self._dirty or old_value != value
|
||||||
|
self._save()
|
||||||
|
|
||||||
|
def getAllUserSettings(self, username):
|
||||||
|
if not username in self._users.key():
|
||||||
|
raise UnknownUser(username)
|
||||||
|
|
||||||
|
user = self._users[username]
|
||||||
|
return user.get_all_settings()
|
||||||
|
|
||||||
def getUserSetting(self, username, key):
|
def getUserSetting(self, username, key):
|
||||||
if not username in self._users.keys():
|
if not username in self._users.keys():
|
||||||
raise UnknownUser(username)
|
raise UnknownUser(username)
|
||||||
|
|
@ -399,11 +427,47 @@ class User(UserMixin):
|
||||||
def is_admin(self):
|
def is_admin(self):
|
||||||
return "admin" in self._roles
|
return "admin" in self._roles
|
||||||
|
|
||||||
|
def get_all_settings(self):
|
||||||
|
return self._settings
|
||||||
|
|
||||||
def get_setting(self, key):
|
def get_setting(self, key):
|
||||||
return self._settings[key] if key in self._settings else None
|
if not isinstance(key, (tuple, list)):
|
||||||
|
path = [key]
|
||||||
|
else:
|
||||||
|
path = key
|
||||||
|
|
||||||
|
return self._get_setting(path)
|
||||||
|
|
||||||
def set_setting(self, key, value):
|
def set_setting(self, key, value):
|
||||||
self._settings[key] = value
|
if not isinstance(key, (tuple, list)):
|
||||||
|
path = [key]
|
||||||
|
else:
|
||||||
|
path = key
|
||||||
|
self._set_setting(path, value)
|
||||||
|
|
||||||
|
def _get_setting(self, path):
|
||||||
|
s = self._settings
|
||||||
|
for p in path:
|
||||||
|
if p in s:
|
||||||
|
s = s[p]
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
return s
|
||||||
|
|
||||||
|
def _set_setting(self, path, value):
|
||||||
|
s = self._settings
|
||||||
|
for p in path[:-1]:
|
||||||
|
if not p in s:
|
||||||
|
s[p] = dict()
|
||||||
|
|
||||||
|
if not isinstance(s[p], dict):
|
||||||
|
return False
|
||||||
|
|
||||||
|
s = s[p]
|
||||||
|
|
||||||
|
key = path[-1]
|
||||||
|
s[key] = value
|
||||||
|
return True
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "User(id=%s,name=%s,active=%r,user=%r,admin=%r)" % (self.get_id(), self.get_name(), self.is_active(), self.is_user(), self.is_admin())
|
return "User(id=%s,name=%s,active=%r,user=%r,admin=%r)" % (self.get_id(), self.get_name(), self.is_active(), self.is_user(), self.is_admin())
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue