WIP on introducing user settings (incl. interface language)

This commit is contained in:
Gina Häußge 2015-03-18 11:30:08 +01:00
parent 7bd83e7ef3
commit f98ebaafee
20 changed files with 457 additions and 69 deletions

View file

@ -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)

View file

@ -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

View file

@ -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"])

View file

@ -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):

View file

@ -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():

View file

@ -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": []
}
}

View file

@ -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?
});

View file

@ -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");
}
});
};

View file

@ -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) {

View 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"]
]);
});

View file

@ -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>

View file

@ -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>

View 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">&times;</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>

View 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>

View file

@ -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>

View file

@ -123,6 +123,7 @@
{% include 'dialogs/firstrun.jinja2' %}
{% include 'dialogs/settings.jinja2' %}
{% include 'dialogs/slicing.jinja2' %}
{% include 'dialogs/usersettings.jinja2' %}
<!-- End of dialogs -->
<!-- Overlays -->

View file

@ -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 = [];

View file

@ -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>

View file

@ -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())