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
|
||||
from sockjs.tornado import SockJSRouter
|
||||
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.babel import Babel, gettext, ngettext
|
||||
from babel import Locale
|
||||
|
|
@ -104,11 +104,25 @@ def after_request(response):
|
|||
def get_locale():
|
||||
if "l10n" in request.values:
|
||||
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)
|
||||
|
||||
|
||||
@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():
|
||||
|
||||
#~~ 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_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
|
||||
#~~ prepare assets
|
||||
|
||||
supported_stylesheets = ("css", "less")
|
||||
assets = dict(
|
||||
|
|
@ -141,7 +156,8 @@ def index():
|
|||
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/log.js'),
|
||||
url_for('static', filename='js/app/viewmodels/usersettings.js')
|
||||
]
|
||||
if enable_gcodeviewer:
|
||||
assets["js"] += [
|
||||
|
|
@ -185,8 +201,10 @@ def index():
|
|||
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
|
||||
|
||||
|
|
@ -244,6 +262,14 @@ def index():
|
|||
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)
|
||||
|
|
@ -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)),
|
||||
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)
|
||||
)
|
||||
|
||||
|
|
@ -273,7 +300,7 @@ def index():
|
|||
|
||||
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]:
|
||||
if t == "navbar" or t == "generic":
|
||||
data = include
|
||||
|
|
@ -292,7 +319,7 @@ def index():
|
|||
# 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 ("navbar", "sidebar", "tab", "settings", "generic"):
|
||||
for t in template_types:
|
||||
configured_order = settings().get(["appearance", "components", "order", t], merged=True)
|
||||
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]
|
||||
|
|
@ -309,7 +336,7 @@ def index():
|
|||
sorted_missing = sorted(missing_in_order, key=lambda x: templates[t]["entries"][x][0])
|
||||
if t == "navbar":
|
||||
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
|
||||
elif t == "settings":
|
||||
templates[t]["entries"]["section_plugins"] = (gettext("Plugins"), None)
|
||||
|
|
@ -331,7 +358,8 @@ def index():
|
|||
uiApiKey=UI_API_KEY,
|
||||
templates=templates,
|
||||
assets=assets,
|
||||
pluginNames=plugin_names
|
||||
pluginNames=plugin_names,
|
||||
locales=locales
|
||||
)
|
||||
render_kwargs.update(plugin_vars)
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import logging
|
|||
import netaddr
|
||||
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.principal import Identity, identity_changed, AnonymousIdentity
|
||||
|
||||
|
|
@ -20,7 +20,7 @@ import octoprint.plugin
|
|||
from octoprint.server import admin_permission, NO_CONTENT
|
||||
from octoprint.settings import settings as s, valid_boolean_trues
|
||||
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
|
||||
|
|
@ -180,40 +180,14 @@ def login():
|
|||
if octoprint.server.userManager is not None:
|
||||
user = octoprint.server.userManager.login_user(user)
|
||||
session["usersession.id"] = user.get_session()
|
||||
g.user = user
|
||||
login_user(user, remember=remember)
|
||||
identity_changed.send(current_app._get_current_object(), identity=Identity(user.get_id()))
|
||||
return jsonify(user.asDict())
|
||||
return make_response(("User unknown or password incorrect", 401, []))
|
||||
|
||||
elif "passive" in request.values.keys():
|
||||
if octoprint.server.userManager is not None:
|
||||
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))
|
||||
elif "passive" in request.values:
|
||||
return passive_login()
|
||||
return NO_CONTENT
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -39,7 +39,8 @@ def getSettings():
|
|||
"appearance": {
|
||||
"name": s.get(["appearance", "name"]),
|
||||
"color": s.get(["appearance", "color"]),
|
||||
"colorTransparent": s.getBoolean(["appearance", "colorTransparent"])
|
||||
"colorTransparent": s.getBoolean(["appearance", "colorTransparent"]),
|
||||
"defaultLanguage": s.get(["appearance", "defaultLanguage"])
|
||||
},
|
||||
"printer": {
|
||||
"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 "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 "defaultLanguage" in data["appearance"]: s.set(["appearance", "defaultLanguage"], data["appearance"]["defaultLanguage"])
|
||||
|
||||
if "printer" in data.keys():
|
||||
if "defaultExtrusionLength" in data["printer"]: s.setInt(["printerParameters", "defaultExtrusionLength"], data["printer"]["defaultExtrusionLength"])
|
||||
|
|
|
|||
|
|
@ -148,6 +148,40 @@ def changePasswordForUser(username):
|
|||
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"])
|
||||
@restricted_access
|
||||
def deleteApikeyForUser(username):
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import time
|
|||
import uuid
|
||||
import threading
|
||||
import logging
|
||||
import netaddr
|
||||
|
||||
from octoprint.settings import settings
|
||||
import octoprint.server
|
||||
|
|
@ -23,11 +24,46 @@ import octoprint.users
|
|||
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 = 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):
|
||||
@functools.wraps(f)
|
||||
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")
|
||||
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
|
||||
if not callable(refreshif) or not refreshif():
|
||||
|
|
|
|||
|
|
@ -168,6 +168,7 @@ default_settings = {
|
|||
"name": "",
|
||||
"color": "default",
|
||||
"colorTransparent": False,
|
||||
"defaultLanguage": None,
|
||||
"components": {
|
||||
"order": {
|
||||
"navbar": ["settings", "systemmenu", "login"],
|
||||
|
|
@ -178,6 +179,7 @@ default_settings = {
|
|||
"section_features", "features", "webcam", "accesscontrol", "api",
|
||||
"section_octoprint", "folders", "appearance", "logs"
|
||||
],
|
||||
"usersettings": ["access", "interface"],
|
||||
"generic": []
|
||||
},
|
||||
"disabled": {
|
||||
|
|
@ -185,6 +187,7 @@ default_settings = {
|
|||
"sidebar": [],
|
||||
"tab": [],
|
||||
"settings": [],
|
||||
"usersettings": [],
|
||||
"generic": []
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ $(function() {
|
|||
self.loginState = parameters[0];
|
||||
self.appearance = parameters[1];
|
||||
self.settings = parameters[2];
|
||||
self.users = parameters[3];
|
||||
self.usersettings = parameters[3];
|
||||
|
||||
self.systemActions = self.settings.system_actions;
|
||||
|
||||
|
|
@ -54,9 +54,7 @@ $(function() {
|
|||
|
||||
OCTOPRINT_VIEWMODELS.push([
|
||||
NavigationViewModel,
|
||||
["loginStateViewModel", "appearanceViewModel", "settingsViewModel", "usersViewModel"],
|
||||
["loginStateViewModel", "appearanceViewModel", "settingsViewModel", "userSettingsViewModel"],
|
||||
"#navbar"
|
||||
]);
|
||||
|
||||
// TODO usersViewModel is needed for what?
|
||||
});
|
||||
|
|
@ -13,6 +13,7 @@ $(function() {
|
|||
self.appearance_name = ko.observable(undefined);
|
||||
self.appearance_color = ko.observable(undefined);
|
||||
self.appearance_colorTransparent = ko.observable();
|
||||
self.appearance_defaultLanguage = ko.observable();
|
||||
|
||||
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.webcam_streamUrl = ko.observable(undefined);
|
||||
|
|
@ -187,6 +194,11 @@ $(function() {
|
|||
self.appearance_name(response.appearance.name);
|
||||
self.appearance_color(response.appearance.color);
|
||||
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);
|
||||
|
||||
|
|
@ -255,7 +267,8 @@ $(function() {
|
|||
"appearance" : {
|
||||
"name": self.appearance_name(),
|
||||
"color": self.appearance_color(),
|
||||
"colorTransparent": self.appearance_colorTransparent()
|
||||
"colorTransparent": self.appearance_colorTransparent(),
|
||||
"defaultLanguage": self.appearance_defaultLanguage()
|
||||
},
|
||||
"printer": {
|
||||
"defaultExtrusionLength": self.printer_defaultExtrusionLength()
|
||||
|
|
@ -326,7 +339,7 @@ $(function() {
|
|||
data: JSON.stringify(data),
|
||||
success: function(response) {
|
||||
self.fromResponse(response);
|
||||
$("#settings_dialog").modal("hide");
|
||||
self.settingsDialog.modal("hide");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -33,6 +33,10 @@ $(function() {
|
|||
self.editorAdmin = ko.observable(undefined);
|
||||
self.editorActive = ko.observable(undefined);
|
||||
|
||||
self.addUserDialog = undefined;
|
||||
self.editUserDialog = undefined;
|
||||
self.changePasswordDialog = undefined;
|
||||
|
||||
self.currentUser.subscribe(function(newValue) {
|
||||
if (newValue === undefined) {
|
||||
self.editorUsername(undefined);
|
||||
|
|
@ -79,7 +83,7 @@ $(function() {
|
|||
|
||||
self.currentUser(undefined);
|
||||
self.editorActive(true);
|
||||
$("#settings-usersDialogAddUser").modal("show");
|
||||
self.addUserDialog.modal("show");
|
||||
};
|
||||
|
||||
self.confirmAddUser = function() {
|
||||
|
|
@ -89,7 +93,7 @@ $(function() {
|
|||
self.addUser(user, function() {
|
||||
// close dialog
|
||||
self.currentUser(undefined);
|
||||
$("#settings-usersDialogAddUser").modal("hide");
|
||||
self.addUserDialog.modal("hide");
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -97,7 +101,7 @@ $(function() {
|
|||
if (!CONFIG_ACCESS_CONTROL) return;
|
||||
|
||||
self.currentUser(user);
|
||||
$("#settings-usersDialogEditUser").modal("show");
|
||||
self.editUserDialog.modal("show");
|
||||
};
|
||||
|
||||
self.confirmEditUser = function() {
|
||||
|
|
@ -111,7 +115,7 @@ $(function() {
|
|||
self.updateUser(user, function() {
|
||||
// close dialog
|
||||
self.currentUser(undefined);
|
||||
$("#settings-usersDialogEditUser").modal("hide");
|
||||
self.editUserDialog.modal("hide");
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -119,7 +123,7 @@ $(function() {
|
|||
if (!CONFIG_ACCESS_CONTROL) return;
|
||||
|
||||
self.currentUser(user);
|
||||
$("#settings-usersDialogChangePassword").modal("show");
|
||||
self.changePasswordDialog.modal("show");
|
||||
};
|
||||
|
||||
self.confirmChangePassword = function() {
|
||||
|
|
@ -128,7 +132,7 @@ $(function() {
|
|||
self.updatePassword(self.currentUser().name, self.editorPassword(), function() {
|
||||
// close dialog
|
||||
self.currentUser(undefined);
|
||||
$("#settings-usersDialogChangePassword").modal("hide");
|
||||
self.changePasswordDialog.modal("hide");
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -153,6 +157,14 @@ $(function() {
|
|||
self.requestData();
|
||||
};
|
||||
|
||||
//~~ Framework
|
||||
|
||||
self.onStartup = function() {
|
||||
self.addUserDialog = $("#settings-usersDialogAddUser");
|
||||
self.editUserDialog = $("#settings-usersDialogEditUser");
|
||||
self.changePasswordDialog = $("#settings-usersDialogChangePassword");
|
||||
};
|
||||
|
||||
//~~ AJAX calls
|
||||
|
||||
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 class="modal-footer">
|
||||
<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>
|
||||
|
||||
|
|
@ -144,14 +144,6 @@
|
|||
|
||||
</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>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -19,4 +19,15 @@
|
|||
</label>
|
||||
</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>
|
||||
|
|
|
|||
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/settings.jinja2' %}
|
||||
{% include 'dialogs/slicing.jinja2' %}
|
||||
{% include 'dialogs/usersettings.jinja2' %}
|
||||
<!-- End of dialogs -->
|
||||
|
||||
<!-- Overlays -->
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
var VERSION = "{{ version }}";
|
||||
var DISPLAY_VERSION = "{{ display_version }}";
|
||||
var LOCALE = "{{ g.locale }}";
|
||||
var AVAILABLE_LOCALES = {{ locales|tojson }};
|
||||
|
||||
var OCTOPRINT_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>
|
||||
</div>
|
||||
<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>
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -127,9 +127,15 @@ class UserManager(object):
|
|||
def getUserSetting(self, username, key):
|
||||
return None
|
||||
|
||||
def getAllUserSettings(self, username):
|
||||
return dict()
|
||||
|
||||
def changeUserSetting(self, username, key, value):
|
||||
pass
|
||||
|
||||
def changeUserSettings(self, username, new_settings):
|
||||
pass
|
||||
|
||||
def removeUser(self, username):
|
||||
if username in self._session_users_by_username:
|
||||
users = self._session_users_by_username[username]
|
||||
|
|
@ -181,7 +187,10 @@ class FilebasedUserManager(UserManager):
|
|||
apikey = None
|
||||
if "apikey" in attributes:
|
||||
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:
|
||||
self._customized = False
|
||||
|
||||
|
|
@ -212,7 +221,7 @@ class FilebasedUserManager(UserManager):
|
|||
if username in self._users.keys():
|
||||
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._save()
|
||||
|
||||
|
|
@ -277,10 +286,29 @@ class FilebasedUserManager(UserManager):
|
|||
user = self._users[username]
|
||||
current = user.get_setting(key)
|
||||
if not current or current != value:
|
||||
old_value = user.get_setting(key)
|
||||
user.set_setting(key, value)
|
||||
self._dirty = True
|
||||
self._dirty = self._dirty or old_value != value
|
||||
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):
|
||||
if not username in self._users.keys():
|
||||
raise UnknownUser(username)
|
||||
|
|
@ -399,11 +427,47 @@ class User(UserMixin):
|
|||
def is_admin(self):
|
||||
return "admin" in self._roles
|
||||
|
||||
def get_all_settings(self):
|
||||
return self._settings
|
||||
|
||||
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):
|
||||
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):
|
||||
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