From 3e5a6d3679674aa5024a5411edeb8e86a45a95b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Fri, 12 Apr 2013 23:08:14 +0200 Subject: [PATCH] Furhter work on user management --- octoprint/server.py | 155 ++++++++++++++++------ octoprint/static/css/octoprint.less | 29 +++- octoprint/static/js/ui.js | 197 ++++++++++++++++++++++++++-- octoprint/templates/index.html | 26 ++-- octoprint/templates/settings.html | 149 ++++++++++++++++++++- octoprint/users.py | 94 +++++++++---- 6 files changed, 556 insertions(+), 94 deletions(-) diff --git a/octoprint/server.py b/octoprint/server.py index 9f33c21..289f8e2 100644 --- a/octoprint/server.py +++ b/octoprint/server.py @@ -122,31 +122,29 @@ def index(): #~~ Printer control -@app.route(BASEURL + "control/connectionOptions", methods=["GET"]) +@app.route(BASEURL + "control/connection/options", methods=["GET"]) def connectionOptions(): return jsonify(getConnectionOptions()) -@app.route(BASEURL + "control/connect", methods=["POST"]) +@app.route(BASEURL + "control/connection", methods=["POST"]) @login_required def connect(): - port = None - baudrate = None - if "port" in request.values.keys(): - port = request.values["port"] - if "baudrate" in request.values.keys(): - baudrate = request.values["baudrate"] - if "save" in request.values.keys(): - settings().set(["serial", "port"], port) - settings().setInt(["serial", "baudrate"], baudrate) - settings().save() - printer.connect(port=port, baudrate=baudrate) - return jsonify(state="Connecting") + if "command" in request.values.keys() and request.values["command"] == "connect": + port = None + baudrate = None + if "port" in request.values.keys(): + port = request.values["port"] + if "baudrate" in request.values.keys(): + baudrate = request.values["baudrate"] + if "save" in request.values.keys(): + settings().set(["serial", "port"], port) + settings().setInt(["serial", "baudrate"], baudrate) + settings().save() + printer.connect(port=port, baudrate=baudrate) + elif "command" in request.values.keys() and request.values["command"] == "disconnect": + printer.disconnect() -@app.route(BASEURL + "control/disconnect", methods=["POST"]) -@login_required -def disconnect(): - printer.disconnect() - return jsonify(state="Offline") + return jsonify(SUCCESS) @app.route(BASEURL + "control/command", methods=["POST"]) @login_required @@ -172,36 +170,27 @@ def printerCommand(): return jsonify(SUCCESS) -@app.route(BASEURL + "control/print", methods=["POST"]) +@app.route(BASEURL + "control/job", methods=["POST"]) @login_required -def printGcode(): - printer.startPrint() - return jsonify(SUCCESS) - -@app.route(BASEURL + "control/pause", methods=["POST"]) -@login_required -def pausePrint(): - printer.togglePausePrint() - return jsonify(SUCCESS) - -@app.route(BASEURL + "control/cancel", methods=["POST"]) -@login_required -def cancelPrint(): - printer.cancelPrint() +def printJobControl(): + if "command" in request.values.keys(): + if request.values["command"] == "start": + printer.startPrint() + elif request.values["command"] == "pause": + printer.togglePausePrint() + elif request.values["command"] == "cancel": + printer.cancelPrint() return jsonify(SUCCESS) @app.route(BASEURL + "control/temperature", methods=["POST"]) @login_required def setTargetTemperature(): - if not printer.isOperational(): - return jsonify(SUCCESS) - - elif request.values.has_key("temp"): - # set target temperature + if "temp" in request.values.keys(): + # set target temperature temp = request.values["temp"] printer.command("M104 S" + temp) - elif request.values.has_key("bedTemp"): + if "bedTemp" in request.values.keys(): # set target bed temperature bedTemp = request.values["bedTemp"] printer.command("M140 S" + bedTemp) @@ -342,7 +331,7 @@ def deleteTimelapse(filename): os.remove(secure) return getTimelapseData() -@app.route(BASEURL + "timelapse/config", methods=["POST"]) +@app.route(BASEURL + "timelapse", methods=["POST"]) @login_required def setTimelapseConfig(): if request.values.has_key("type"): @@ -451,6 +440,90 @@ def setSettings(): return getSettings() +#~~ user settings + +@app.route(BASEURL + "users", methods=["GET"]) +@login_required +@admin_permission.require() +def getUsers(): + return jsonify({"users": userManager.getAllUsers()}) + +@app.route(BASEURL + "users", methods=["POST"]) +@login_required +@admin_permission.require() +def addUser(): + if "application/json" in request.headers["Content-Type"]: + data = request.json + + name = data["name"] + password = data["password"] + active = data["active"] + + roles = ["user"] + if "admin" in data.keys() and data["admin"]: + roles.append("admin") + + try: + userManager.addUser(name, password, active, roles) + except users.UserAlreadyExists: + return app.make_response(("User already exists: " % name, 409, [])) + return getUsers() + +@app.route(BASEURL + "users/", methods=["GET"]) +@login_required +@admin_permission.require() +def getUser(username): + user = userManager.findUser(username) + if user is not None: + return jsonify(user.asDict()) + else: + return app.make_response(("Unknown user: " % username, 404, [])) + +@app.route(BASEURL + "users/", methods=["PUT"]) +@login_required +@admin_permission.require() +def updateUser(username): + user = userManager.findUser(username) + if user is not None: + if "application/json" in request.headers["Content-Type"]: + data = request.json + + # change roles + roles = ["user"] + if "admin" in data.keys() and data["admin"]: + roles.append("admin") + userManager.changeUserRoles(username, roles) + + # change activation + if "active" in data.keys(): + userManager.changeUserActivation(username, data["active"]) + return getUsers() + else: + return app.make_response(("Unknown user: " % username, 404, [])) + +@app.route(BASEURL + "users/", methods=["DELETE"]) +@login_required +@admin_permission.require() +def removeUser(username): + try: + userManager.removeUser(username) + return getUsers() + except users.UnknownUser: + return app.make_response(("Unknown user: " % username, 404, [])) + +@app.route(BASEURL + "users//password", methods=["PUT"]) +@login_required +@admin_permission.require() +def changePasswordForUser(username): + if "application/json" in request.headers["Content-Type"]: + data = request.json + if "password" in data.keys() and data["password"]: + try: + userManager.changeUserPassword(username, data["password"]) + except users.UnknownUser: + return app.make_response(("Unknown user: " % username, 404, [])) + return jsonify(SUCCESS) + #~~ system control @app.route(BASEURL + "system", methods=["POST"]) diff --git a/octoprint/static/css/octoprint.less b/octoprint/static/css/octoprint.less index 2822bde..cace470 100644 --- a/octoprint/static/css/octoprint.less +++ b/octoprint/static/css/octoprint.less @@ -112,7 +112,7 @@ body { .octoprint-container { .accordion-heading { .settings-trigger { - float: right; + //float: right; padding: 0px 15px; } @@ -192,6 +192,32 @@ table { } } } + + // user settings + &.settings_users_name { + text-overflow: ellipsis; + text-align: left; + } + + &.settings_users_active, &.settings_users_admin { + text-align: center; + width: 55px; + } + + &.settings_users_actions { + text-align: center; + width: 60px; + + a { + text-decoration: none; + color: #000; + + &.disabled { + color: #ccc; + cursor: default; + } + } + } } } @@ -334,7 +360,6 @@ ul.dropdown-menu li a { /** Settings dialog */ #settings_dialog { - width: 650px; } /** Footer */ diff --git a/octoprint/static/js/ui.js b/octoprint/static/js/ui.js index 6bef7b8..d35ebdc 100644 --- a/octoprint/static/js/ui.js +++ b/octoprint/static/js/ui.js @@ -100,7 +100,7 @@ function ConnectionViewModel(loginStateViewModel) { self.requestData = function() { $.ajax({ - url: AJAX_BASEURL + "control/connectionOptions", + url: AJAX_BASEURL + "control/connection/options", method: "GET", dataType: "json", success: function(response) { @@ -155,6 +155,7 @@ function ConnectionViewModel(loginStateViewModel) { self.connect = function() { if (self.isErrorOrClosed()) { var data = { + "command": "connect", "port": self.selectedPort(), "baudrate": self.selectedBaudrate() }; @@ -163,7 +164,7 @@ function ConnectionViewModel(loginStateViewModel) { data["save"] = true; $.ajax({ - url: AJAX_BASEURL + "control/connect", + url: AJAX_BASEURL + "control/connection", type: "POST", dataType: "json", data: data @@ -171,9 +172,10 @@ function ConnectionViewModel(loginStateViewModel) { } else { self.requestData(); $.ajax({ - url: AJAX_BASEURL + "control/disconnect", + url: AJAX_BASEURL + "control/connection", type: "POST", - dataType: "json" + dataType: "json", + data: {"command": "disconnect"} }) } } @@ -1095,11 +1097,181 @@ function GcodeViewModel(loginStateViewModel) { } -function SettingsViewModel(loginStateViewModel) { +function UsersViewModel(loginStateViewModel) { var self = this; self.loginState = loginStateViewModel; + // initialize list helper + self.listHelper = new ItemListHelper( + "users", + { + "name": function(a, b) { + // sorts ascending + if (a["name"].toLocaleLowerCase() < b["name"].toLocaleLowerCase()) return -1; + if (a["name"].toLocaleLowerCase() > b["name"].toLocaleLowerCase()) return 1; + return 0; + } + }, + {}, + "name", + [], + CONFIG_USERSPERPAGE + ) + + self.emptyUser = {name: "", admin: false, active: false}; + + self.currentUser = ko.observable(self.emptyUser); + + self.editorUsername = ko.observable(undefined); + self.editorPassword = ko.observable(undefined); + self.editorRepeatedPassword = ko.observable(undefined); + self.editorAdmin = ko.observable(undefined); + self.editorActive = ko.observable(undefined); + + self.currentUser.subscribe(function(newValue) { + if (newValue === undefined) { + self.editorUsername(undefined); + self.editorAdmin(undefined); + self.editorActive(undefined); + } else { + self.editorUsername(newValue.name); + self.editorAdmin(newValue.admin); + self.editorActive(newValue.active); + } + self.editorPassword(undefined); + self.editorRepeatedPassword(undefined); + }); + + self.editorPasswordMismatch = ko.computed(function() { + return self.editorPassword() != self.editorRepeatedPassword(); + }); + + self.requestData = function() { + $.ajax({ + url: AJAX_BASEURL + "users", + type: "GET", + dataType: "json", + success: self.fromResponse + }); + } + + self.fromResponse = function(response) { + self.listHelper.updateItems(response.users); + } + + self.showAddUserDialog = function() { + self.currentUser(undefined); + $("#settings-usersDialogAddUser").modal("show"); + } + + self.confirmAddUser = function() { + var user = {name: self.editorUsername(), password: self.editorPassword(), admin: self.editorAdmin(), active: self.editorActive()}; + self.addUser(user, function() { + // close dialog + self.currentUser(undefined); + $("#settings-usersDialogAddUser").modal("hide"); + }); + } + + self.showEditUserDialog = function(user) { + self.currentUser(user); + $("#settings-usersDialogEditUser").modal("show"); + } + + self.confirmEditUser = function() { + var user = self.currentUser(); + user.active = self.editorActive(); + user.admin = self.editorAdmin(); + + // make AJAX call + self.updateUser(user, function() { + // close dialog + self.currentUser(undefined); + $("#settings-usersDialogEditUser").modal("hide"); + }); + } + + self.showChangePasswordDialog = function(user) { + self.currentUser(user); + $("#settings-usersDialogChangePassword").modal("show"); + } + + self.confirmChangePassword = function() { + self.updatePassword(self.currentUser().name, self.editorPassword(), function() { + // close dialog + self.currentUser(undefined); + $("#settings-usersDialogChangePassword").modal("hide"); + }); + } + + //~~ AJAX calls + + self.addUser = function(user, callback) { + if (user === undefined) return; + + $.ajax({ + url: AJAX_BASEURL + "users", + type: "POST", + contentType: "application/json; charset=UTF-8", + data: JSON.stringify(user), + success: function(response) { + self.fromResponse(response); + callback(); + } + }); + } + + self.removeUser = function(user, callback) { + if (user === undefined) return; + if (user.name == loginStateViewModel.username()) { + // we do not allow to delete ourself + $.pnotify({title: "Not possible", text: "You may not delete your own account.", type: "error"}); + return; + } + + $.ajax({ + url: AJAX_BASEURL + "users/" + user.name, + type: "DELETE", + success: function(response) { + self.fromResponse(response); + callback(); + } + }); + } + + self.updateUser = function(user, callback) { + if (user === undefined) return; + + $.ajax({ + url: AJAX_BASEURL + "users/" + user.name, + type: "PUT", + contentType: "application/json; charset=UTF-8", + data: JSON.stringify(user), + success: function(response) { + self.fromResponse(response); + callback(); + } + }); + } + + self.updatePassword = function(username, password, callback) { + $.ajax({ + url: AJAX_BASEURL + "users/" + username + "/password", + type: "PUT", + contentType: "application/json; charset=UTF-8", + data: JSON.stringify({password: password}), + success: callback + }); + } +} + +function SettingsViewModel(loginStateViewModel, usersViewModel) { + var self = this; + + self.loginState = loginStateViewModel; + self.users = usersViewModel; + self.appearance_name = ko.observable(undefined); self.appearance_color = ko.observable(undefined); @@ -1143,7 +1315,8 @@ function SettingsViewModel(loginStateViewModel) { type: "GET", dataType: "json", success: self.fromResponse - }) + }); + self.users.requestData(); } self.fromResponse = function(response) { @@ -1574,10 +1747,11 @@ function AppearanceViewModel(settingsViewModel) { $(function() { //~~ View models - var loginStateViewModel = new LoginStateViewModel(); + var loginStateViewModel = new LoginStateViewModel(loginStateViewModel); + var usersViewModel = new UsersViewModel(loginStateViewModel); var connectionViewModel = new ConnectionViewModel(loginStateViewModel); var printerStateViewModel = new PrinterStateViewModel(loginStateViewModel); - var settingsViewModel = new SettingsViewModel(loginStateViewModel); + var settingsViewModel = new SettingsViewModel(loginStateViewModel, usersViewModel); var appearanceViewModel = new AppearanceViewModel(settingsViewModel); var temperatureViewModel = new TemperatureViewModel(loginStateViewModel, settingsViewModel); var controlViewModel = new ControlsViewModel(loginStateViewModel); @@ -1618,7 +1792,7 @@ $(function() { return false; }) - //~~ Print job control + //~~ Print job control (should move to PrinterStateViewModel) //~~ Temperature control (should really move to knockout click binding) @@ -1779,6 +1953,11 @@ $(function() { $.pnotify.defaults.history = false; + $.fn.modal.defaults.maxHeight = function(){ + // subtract the height of the modal header and footer + return $(window).height() - 165; + } + // Fix input element click problem on login dialog $('.dropdown input, .dropdown label').click(function(e) { e.stopPropagation(); diff --git a/octoprint/templates/index.html b/octoprint/templates/index.html index 632ed60..64f47f8 100644 --- a/octoprint/templates/index.html +++ b/octoprint/templates/index.html @@ -8,6 +8,7 @@ + @@ -19,6 +20,7 @@ var AJAX_BASEURL = "/ajax/"; var CONFIG_GCODEFILESPERPAGE = 5; var CONFIG_TIMELAPSEFILESPERPAGE = 10; + var CONFIG_USERSPERPAGE = 10; var CONFIG_WEBCAM_STREAM = "{{ webcamStream }}"; var WEB_SOCKET_SWF_LOCATION = "{{ url_for('static', filename='js/WebSocketMain.swf') }}"; @@ -128,7 +130,7 @@ - - - - - + + + + + - - - - - + + + + +
NameSizeAction
NameSizeAction
 | 
 |