diff --git a/src/octoprint/server/__init__.py b/src/octoprint/server/__init__.py index c9a6daec..d3d48ee1 100644 --- a/src/octoprint/server/__init__.py +++ b/src/octoprint/server/__init__.py @@ -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) diff --git a/src/octoprint/server/api/__init__.py b/src/octoprint/server/api/__init__.py index 5c4a1fc9..29bf958a 100644 --- a/src/octoprint/server/api/__init__.py +++ b/src/octoprint/server/api/__init__.py @@ -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 diff --git a/src/octoprint/server/api/settings.py b/src/octoprint/server/api/settings.py index 6da2cc9a..bd91e286 100644 --- a/src/octoprint/server/api/settings.py +++ b/src/octoprint/server/api/settings.py @@ -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"]) diff --git a/src/octoprint/server/api/users.py b/src/octoprint/server/api/users.py index cf2e7fd3..1d442e4c 100644 --- a/src/octoprint/server/api/users.py +++ b/src/octoprint/server/api/users.py @@ -148,6 +148,40 @@ def changePasswordForUser(username): return make_response(("Forbidden", 403, [])) +@api.route("/users//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//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//apikey", methods=["DELETE"]) @restricted_access def deleteApikeyForUser(username): diff --git a/src/octoprint/server/util/flask.py b/src/octoprint/server/util/flask.py index c52f4174..1d0bbe6f 100644 --- a/src/octoprint/server/util/flask.py +++ b/src/octoprint/server/util/flask.py @@ -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(): diff --git a/src/octoprint/settings.py b/src/octoprint/settings.py index 25f92de4..3be76c78 100644 --- a/src/octoprint/settings.py +++ b/src/octoprint/settings.py @@ -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": [] } } diff --git a/src/octoprint/static/js/app/viewmodels/navigation.js b/src/octoprint/static/js/app/viewmodels/navigation.js index 6ea94569..dff18142 100644 --- a/src/octoprint/static/js/app/viewmodels/navigation.js +++ b/src/octoprint/static/js/app/viewmodels/navigation.js @@ -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? }); \ No newline at end of file diff --git a/src/octoprint/static/js/app/viewmodels/settings.js b/src/octoprint/static/js/app/viewmodels/settings.js index 30da456e..4e003094 100644 --- a/src/octoprint/static/js/app/viewmodels/settings.js +++ b/src/octoprint/static/js/app/viewmodels/settings.js @@ -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"); } }); }; diff --git a/src/octoprint/static/js/app/viewmodels/users.js b/src/octoprint/static/js/app/viewmodels/users.js index 0cb04c36..eae49bc1 100644 --- a/src/octoprint/static/js/app/viewmodels/users.js +++ b/src/octoprint/static/js/app/viewmodels/users.js @@ -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) { diff --git a/src/octoprint/static/js/app/viewmodels/usersettings.js b/src/octoprint/static/js/app/viewmodels/usersettings.js new file mode 100644 index 00000000..f5498d20 --- /dev/null +++ b/src/octoprint/static/js/app/viewmodels/usersettings.js @@ -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"] + ]); +}); \ No newline at end of file diff --git a/src/octoprint/templates/dialogs/_snippets/changepassword.jinja2 b/src/octoprint/templates/dialogs/_snippets/changepassword.jinja2 new file mode 100644 index 00000000..e69de29b diff --git a/src/octoprint/templates/dialogs/settings/accesscontrol.jinja2 b/src/octoprint/templates/dialogs/settings/accesscontrol.jinja2 index 0ab00fdb..023ffe10 100644 --- a/src/octoprint/templates/dialogs/settings/accesscontrol.jinja2 +++ b/src/octoprint/templates/dialogs/settings/accesscontrol.jinja2 @@ -107,7 +107,7 @@ @@ -144,14 +144,6 @@ - diff --git a/src/octoprint/templates/dialogs/settings/appearance.jinja2 b/src/octoprint/templates/dialogs/settings/appearance.jinja2 index 62c57af9..5744d729 100644 --- a/src/octoprint/templates/dialogs/settings/appearance.jinja2 +++ b/src/octoprint/templates/dialogs/settings/appearance.jinja2 @@ -19,4 +19,15 @@ +
+ +
+ + {{ _('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.') }} +
+
diff --git a/src/octoprint/templates/dialogs/usersettings.jinja2 b/src/octoprint/templates/dialogs/usersettings.jinja2 new file mode 100644 index 00000000..00cbcc6c --- /dev/null +++ b/src/octoprint/templates/dialogs/usersettings.jinja2 @@ -0,0 +1,49 @@ + diff --git a/src/octoprint/templates/dialogs/usersettings/access.jinja2 b/src/octoprint/templates/dialogs/usersettings/access.jinja2 new file mode 100644 index 00000000..83e43eb6 --- /dev/null +++ b/src/octoprint/templates/dialogs/usersettings/access.jinja2 @@ -0,0 +1,40 @@ +
+
+ {{ _('Password') }} +
+ +
+ +
+
+
+ +
+ + {{ _('Passwords do not match') }} +
+
+
+
+ {{ _('API Key') }} +
+ +
+
+ + + +
+ +
+
+ +
+ +
+
+
+
+ +
+
diff --git a/src/octoprint/templates/dialogs/usersettings/interface.jinja2 b/src/octoprint/templates/dialogs/usersettings/interface.jinja2 new file mode 100644 index 00000000..55c28f6b --- /dev/null +++ b/src/octoprint/templates/dialogs/usersettings/interface.jinja2 @@ -0,0 +1,15 @@ +
+
+ {{ _('Language') }} +
+
+ + {{ _('Changes to the interface language will only become active after a reload of the page.') }} +
+
+
+
diff --git a/src/octoprint/templates/index.jinja2 b/src/octoprint/templates/index.jinja2 index d7b85295..d7aa1aa6 100644 --- a/src/octoprint/templates/index.jinja2 +++ b/src/octoprint/templates/index.jinja2 @@ -123,6 +123,7 @@ {% include 'dialogs/firstrun.jinja2' %} {% include 'dialogs/settings.jinja2' %} {% include 'dialogs/slicing.jinja2' %} + {% include 'dialogs/usersettings.jinja2' %} diff --git a/src/octoprint/templates/initscript.jinja2 b/src/octoprint/templates/initscript.jinja2 index 3fb3c0a9..2351db39 100644 --- a/src/octoprint/templates/initscript.jinja2 +++ b/src/octoprint/templates/initscript.jinja2 @@ -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 = []; diff --git a/src/octoprint/templates/navbar/login.jinja2 b/src/octoprint/templates/navbar/login.jinja2 index 1b91f272..229fe7f9 100644 --- a/src/octoprint/templates/navbar/login.jinja2 +++ b/src/octoprint/templates/navbar/login.jinja2 @@ -13,6 +13,6 @@ diff --git a/src/octoprint/users.py b/src/octoprint/users.py index 7562bc2f..1aaf2071 100644 --- a/src/octoprint/users.py +++ b/src/octoprint/users.py @@ -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())