User management now working

Also reorganized javascripts a bit (as preparation for some refactoring coming up) and renamed templates from ".html" to ".jinja2".
master
Gina Häußge 2013-04-13 21:40:28 +02:00
parent 244ff25e2f
commit 19dc238f06
22 changed files with 241 additions and 261 deletions

View File

@ -4,7 +4,7 @@ __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agp
from werkzeug.utils import secure_filename
import tornadio2
from flask import Flask, request, render_template, jsonify, send_from_directory, url_for, current_app, session
from flask import Flask, request, render_template, jsonify, send_from_directory, url_for, current_app, session, abort
from flask.ext.login import LoginManager, login_user, logout_user, login_required, current_user
from flask.ext.principal import Principal, Permission, RoleNeed, Identity, identity_changed, AnonymousIdentity, identity_loaded, UserNeed
@ -112,7 +112,8 @@ class PrinterStateConnection(tornadio2.SocketConnection):
@app.route("/")
def index():
return render_template(
"index.html",
"index.jinja2",
ajaxBaseUrl=BASEURL,
webcamStream=settings().get(["webcam", "stream"]),
enableTimelapse=(settings().get(["webcam", "snapshot"]) is not None and settings().get(["webcam", "ffmpeg"]) is not None),
enableGCodeVisualizer=settings().get(["feature", "gCodeVisualizer"]),
@ -397,7 +398,7 @@ def getSettings():
@app.route(BASEURL + "settings", methods=["POST"])
@login_required
@admin_permission.require()
@admin_permission.require(403)
def setSettings():
if "application/json" in request.headers["Content-Type"]:
data = request.json
@ -444,14 +445,20 @@ def setSettings():
@app.route(BASEURL + "users", methods=["GET"])
@login_required
@admin_permission.require()
@admin_permission.require(403)
def getUsers():
if userManager is None:
return jsonify(SUCCESS)
return jsonify({"users": userManager.getAllUsers()})
@app.route(BASEURL + "users", methods=["POST"])
@login_required
@admin_permission.require()
@admin_permission.require(403)
def addUser():
if userManager is None:
return jsonify(SUCCESS)
if "application/json" in request.headers["Content-Type"]:
data = request.json
@ -466,23 +473,31 @@ def addUser():
try:
userManager.addUser(name, password, active, roles)
except users.UserAlreadyExists:
return app.make_response(("User already exists: " % name, 409, []))
abort(409)
return getUsers()
@app.route(BASEURL + "users/<username>", methods=["GET"])
@login_required
@admin_permission.require()
def getUser(username):
user = userManager.findUser(username)
if user is not None:
return jsonify(user.asDict())
if userManager is None:
return jsonify(SUCCESS)
if current_user is not None and not current_user.is_anonymous() and (current_user.get_name() == username or current_user.is_admin()):
user = userManager.findUser(username)
if user is not None:
return jsonify(user.asDict())
else:
abort(404)
else:
return app.make_response(("Unknown user: " % username, 404, []))
abort(403)
@app.route(BASEURL + "users/<username>", methods=["PUT"])
@login_required
@admin_permission.require()
@admin_permission.require(403)
def updateUser(username):
if userManager is None:
return jsonify(SUCCESS)
user = userManager.findUser(username)
if user is not None:
if "application/json" in request.headers["Content-Type"]:
@ -499,36 +514,44 @@ def updateUser(username):
userManager.changeUserActivation(username, data["active"])
return getUsers()
else:
return app.make_response(("Unknown user: " % username, 404, []))
abort(404)
@app.route(BASEURL + "users/<username>", methods=["DELETE"])
@login_required
@admin_permission.require()
@admin_permission.require(http_exception=403)
def removeUser(username):
if userManager is None:
return jsonify(SUCCESS)
try:
userManager.removeUser(username)
return getUsers()
except users.UnknownUser:
return app.make_response(("Unknown user: " % username, 404, []))
abort(404)
@app.route(BASEURL + "users/<username>/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)
if userManager is None:
return jsonify(SUCCESS)
if current_user is not None and not current_user.is_anonymous() and (current_user.get_name() == username or current_user.is_admin()):
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: %s" % username, 404, []))
return jsonify(SUCCESS)
else:
return app.make_response(("Forbidden", 403, []))
#~~ system control
@app.route(BASEURL + "system", methods=["POST"])
@login_required
@admin_permission.require()
@admin_permission.require(403)
def performSystemAction():
logger = logging.getLogger(__name__)
if request.values.has_key("action"):
@ -565,12 +588,12 @@ def login():
if user.check_password(users.UserManager.createPasswordHash(password)):
login_user(user, remember=remember)
identity_changed.send(current_app._get_current_object(), identity=Identity(user.get_id()))
return jsonify({"name": user.get_name(), "user": user.is_user(), "admin": user.is_admin()})
return jsonify(user.asDict())
return app.make_response(("User unknown or password incorrect", 401, []))
elif "passive" in request.values.keys():
user = current_user
if user is not None and not user.is_anonymous():
return jsonify({"name": user.get_name(), "user": user.is_user(), "admin": user.is_admin()})
return jsonify(user.asDict())
else:
return jsonify(SUCCESS)
@ -590,7 +613,11 @@ def logout():
def on_identity_loaded(sender, identity):
user = load_user(identity.name)
if user is None:
return
if userManager is None:
# access control is disabled, we'll create permissions for the DummyUser
user = users.DummyUser()
else:
return
identity.provides.add(UserNeed(user.get_name()))
if user.is_user():

View File

@ -18,7 +18,7 @@
// Register as an anonymous AMD module:
define([
'jquery',
'jquery.ui.widget'
'jquery.ui.widget.js'
], factory);
} else {
// Browser globals:

View File

@ -8,6 +8,8 @@ function LoginStateViewModel() {
self.isAdmin = ko.observable(false);
self.isUser = ko.observable(false);
self.currentUser = ko.observable(undefined);
self.userMenuText = ko.computed(function() {
if (self.loggedIn()) {
return "\"" + self.username() + "\"";
@ -16,6 +18,11 @@ function LoginStateViewModel() {
}
})
self.subscribers = [];
self.subscribe = function(callback) {
self.subscribers.push(callback);
}
self.requestData = function() {
$.ajax({
url: AJAX_BASEURL + "login",
@ -31,11 +38,19 @@ function LoginStateViewModel() {
self.username(response.name);
self.isUser(response.user);
self.isAdmin(response.admin);
self.currentUser(response);
_.each(self.subscribers, function(callback) { callback("login", response); });
} else {
self.loggedIn(false);
self.username(undefined);
self.isUser(false);
self.isAdmin(false);
self.currentUser(undefined);
_.each(self.subscribers, function(callback) { callback("logout", {}); });
}
}
@ -44,12 +59,16 @@ function LoginStateViewModel() {
var password = $("#login_pass").val();
var remember = $("#login_remember").is(":checked");
$("#login_user").val("");
$("#login_pass").val("");
$("#login_remember").prop("checked", false);
$.ajax({
url: AJAX_BASEURL + "login",
type: "POST",
data: {"user": username, "pass": password, "remember": remember},
success: function(response) {
$.pnotify({title: "Login successful", text: "You are now logged in", type: "success"});
$.pnotify({title: "Login successful", text: "You are now logged in as \"" + response.name + "\"", type: "success"});
self.fromResponse(response);
},
error: function(jqXHR, textStatus, errorThrown) {
@ -647,66 +666,6 @@ function ControlViewModel(loginStateViewModel) {
}
function SpeedViewModel(loginStateViewModel) {
var self = this;
self.loginState = loginStateViewModel;
self.outerWall = ko.observable(undefined);
self.innerWall = ko.observable(undefined);
self.fill = ko.observable(undefined);
self.support = ko.observable(undefined);
self.isErrorOrClosed = ko.observable(undefined);
self.isOperational = ko.observable(undefined);
self.isPrinting = ko.observable(undefined);
self.isPaused = ko.observable(undefined);
self.isError = ko.observable(undefined);
self.isReady = ko.observable(undefined);
self.isLoading = ko.observable(undefined);
self._fromCurrentData = function(data) {
self._processStateData(data.state);
}
self._fromHistoryData = function(data) {
self._processStateData(data.state);
}
self._processStateData = function(data) {
self.isErrorOrClosed(data.flags.closedOrError);
self.isOperational(data.flags.operational);
self.isPaused(data.flags.paused);
self.isPrinting(data.flags.printing);
self.isError(data.flags.error);
self.isReady(data.flags.ready);
self.isLoading(data.flags.loading);
}
self.requestData = function() {
$.ajax({
url: AJAX_BASEURL + "control/speed",
type: "GET",
dataType: "json",
success: self._fromResponse
});
}
self._fromResponse = function(response) {
if (response.feedrate) {
self.outerWall(response.feedrate.outerWall);
self.innerWall(response.feedrate.innerWall);
self.fill(response.feedrate.fill);
self.support(response.feedrate.support);
} else {
self.outerWall(undefined);
self.innerWall(undefined);
self.fill(undefined);
self.support(undefined);
}
}
}
function TerminalViewModel(loginStateViewModel) {
var self = this;
@ -822,11 +781,11 @@ function GcodeFilesViewModel(loginStateViewModel) {
);
self.isLoadActionPossible = ko.computed(function() {
return !self.isPrinting() && !self.isPaused() && !self.isLoading();
return self.loginState.isUser() && !self.isPrinting() && !self.isPaused() && !self.isLoading();
});
self.isLoadAndPrintActionPossible = ko.computed(function() {
return self.isOperational() && self.isLoadActionPossible();
return self.loginState.isUser() && self.isOperational() && self.isLoadActionPossible();
});
self.fromCurrentData = function(data) {
@ -1117,7 +1076,7 @@ function UsersViewModel(loginStateViewModel) {
"name",
[],
CONFIG_USERSPERPAGE
)
);
self.emptyUser = {name: "", admin: false, active: false};
@ -1148,6 +1107,8 @@ function UsersViewModel(loginStateViewModel) {
});
self.requestData = function() {
if (!CONFIG_ACCESS_CONTROL) return;
$.ajax({
url: AJAX_BASEURL + "users",
type: "GET",
@ -1161,11 +1122,16 @@ function UsersViewModel(loginStateViewModel) {
}
self.showAddUserDialog = function() {
if (!CONFIG_ACCESS_CONTROL) return;
self.currentUser(undefined);
self.editorActive(true);
$("#settings-usersDialogAddUser").modal("show");
}
self.confirmAddUser = function() {
if (!CONFIG_ACCESS_CONTROL) return;
var user = {name: self.editorUsername(), password: self.editorPassword(), admin: self.editorAdmin(), active: self.editorActive()};
self.addUser(user, function() {
// close dialog
@ -1175,11 +1141,15 @@ function UsersViewModel(loginStateViewModel) {
}
self.showEditUserDialog = function(user) {
if (!CONFIG_ACCESS_CONTROL) return;
self.currentUser(user);
$("#settings-usersDialogEditUser").modal("show");
}
self.confirmEditUser = function() {
if (!CONFIG_ACCESS_CONTROL) return;
var user = self.currentUser();
user.active = self.editorActive();
user.admin = self.editorAdmin();
@ -1193,11 +1163,15 @@ function UsersViewModel(loginStateViewModel) {
}
self.showChangePasswordDialog = function(user) {
if (!CONFIG_ACCESS_CONTROL) return;
self.currentUser(user);
$("#settings-usersDialogChangePassword").modal("show");
}
self.confirmChangePassword = function() {
if (!CONFIG_ACCESS_CONTROL) return;
self.updatePassword(self.currentUser().name, self.editorPassword(), function() {
// close dialog
self.currentUser(undefined);
@ -1208,6 +1182,7 @@ function UsersViewModel(loginStateViewModel) {
//~~ AJAX calls
self.addUser = function(user, callback) {
if (!CONFIG_ACCESS_CONTROL) return;
if (user === undefined) return;
$.ajax({
@ -1223,7 +1198,9 @@ function UsersViewModel(loginStateViewModel) {
}
self.removeUser = function(user, callback) {
if (!CONFIG_ACCESS_CONTROL) return;
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"});
@ -1241,6 +1218,7 @@ function UsersViewModel(loginStateViewModel) {
}
self.updateUser = function(user, callback) {
if (!CONFIG_ACCESS_CONTROL) return;
if (user === undefined) return;
$.ajax({
@ -1256,6 +1234,8 @@ function UsersViewModel(loginStateViewModel) {
}
self.updatePassword = function(username, password, callback) {
if (!CONFIG_ACCESS_CONTROL) return;
$.ajax({
url: AJAX_BASEURL + "users/" + username + "/password",
type: "PUT",
@ -1316,7 +1296,6 @@ function SettingsViewModel(loginStateViewModel, usersViewModel) {
dataType: "json",
success: self.fromResponse
});
self.users.requestData();
}
self.fromResponse = function(response) {
@ -1399,12 +1378,13 @@ function SettingsViewModel(loginStateViewModel, usersViewModel) {
}
function NavigationViewModel(loginStateViewModel, appearanceViewModel, settingsViewModel) {
function NavigationViewModel(loginStateViewModel, appearanceViewModel, settingsViewModel, usersViewModel) {
var self = this;
self.loginState = loginStateViewModel;
self.appearance = appearanceViewModel;
self.systemActions = settingsViewModel.system_actions;
self.users = usersViewModel;
self.triggerAction = function(action) {
var callback = function() {
@ -1431,7 +1411,7 @@ function NavigationViewModel(loginStateViewModel, appearanceViewModel, settingsV
}
}
function DataUpdater(loginStateViewModel, connectionViewModel, printerStateViewModel, temperatureViewModel, controlViewModel, speedViewModel, terminalViewModel, gcodeFilesViewModel, timelapseViewModel, gcodeViewModel) {
function DataUpdater(loginStateViewModel, connectionViewModel, printerStateViewModel, temperatureViewModel, controlViewModel, terminalViewModel, gcodeFilesViewModel, timelapseViewModel, gcodeViewModel) {
var self = this;
self.loginStateViewModel = loginStateViewModel;
@ -1440,7 +1420,6 @@ function DataUpdater(loginStateViewModel, connectionViewModel, printerStateViewM
self.temperatureViewModel = temperatureViewModel;
self.controlViewModel = controlViewModel;
self.terminalViewModel = terminalViewModel;
self.speedViewModel = speedViewModel;
self.gcodeFilesViewModel = gcodeFilesViewModel;
self.timelapseViewModel = timelapseViewModel;
self.gcodeViewModel = gcodeViewModel;
@ -1755,12 +1734,11 @@ $(function() {
var appearanceViewModel = new AppearanceViewModel(settingsViewModel);
var temperatureViewModel = new TemperatureViewModel(loginStateViewModel, settingsViewModel);
var controlViewModel = new ControlsViewModel(loginStateViewModel);
var speedViewModel = new SpeedViewModel(loginStateViewModel);
var terminalViewModel = new TerminalViewModel(loginStateViewModel);
var gcodeFilesViewModel = new GcodeFilesViewModel(loginStateViewModel);
var timelapseViewModel = new TimelapseViewModel(loginStateViewModel);
var gcodeViewModel = new GcodeViewModel(loginStateViewModel);
var navigationViewModel = new NavigationViewModel(loginStateViewModel, appearanceViewModel, settingsViewModel);
var navigationViewModel = new NavigationViewModel(loginStateViewModel, appearanceViewModel, settingsViewModel, usersViewModel);
var dataUpdater = new DataUpdater(
loginStateViewModel,
@ -1768,7 +1746,6 @@ $(function() {
printerStateViewModel,
temperatureViewModel,
controlViewModel,
speedViewModel,
terminalViewModel,
gcodeFilesViewModel,
timelapseViewModel,
@ -1821,28 +1798,6 @@ $(function() {
terminalViewModel.updateOutput();
});
//~~ Speed controls
function speedCommand(structure) {
var speedSetting = $("#speed_" + structure).val();
if (speedSetting) {
$.ajax({
url: AJAX_BASEURL + "control/speed",
type: "POST",
dataType: "json",
data: structure + "=" + speedSetting,
success: function(response) {
$("#speed_" + structure).val("")
speedViewModel.fromResponse(response);
}
})
}
}
$("#speed_outerWall_set").click(function() {speedCommand("outerWall")});
$("#speed_innerWall_set").click(function() {speedCommand("innerWall")});
$("#speed_support_set").click(function() {speedCommand("support")});
$("#speed_fill_set").click(function() {speedCommand("fill")});
//~~ Terminal
$("#terminal-send").click(function () {
@ -1909,14 +1864,12 @@ $(function() {
}
}
ko.applyBindings(connectionViewModel, document.getElementById("connection"));
ko.applyBindings(printerStateViewModel, document.getElementById("state"));
ko.applyBindings(gcodeFilesViewModel, document.getElementById("files"));
ko.applyBindings(gcodeFilesViewModel, document.getElementById("files-heading"));
ko.applyBindings(connectionViewModel, document.getElementById("connection_accordion"));
ko.applyBindings(printerStateViewModel, document.getElementById("state_accordion"));
ko.applyBindings(gcodeFilesViewModel, document.getElementById("files_accordion"));
ko.applyBindings(temperatureViewModel, document.getElementById("temp"));
ko.applyBindings(controlViewModel, document.getElementById("control"));
ko.applyBindings(terminalViewModel, document.getElementById("term"));
ko.applyBindings(speedViewModel, document.getElementById("speed"));
ko.applyBindings(gcodeViewModel, document.getElementById("gcode"));
ko.applyBindings(settingsViewModel, document.getElementById("settings_dialog"));
ko.applyBindings(navigationViewModel, document.getElementById("navbar"));
@ -1930,6 +1883,7 @@ $(function() {
if (gCodeVisualizerElement) {
gcodeViewModel.initialize();
}
//~~ startup commands
loginStateViewModel.requestData();
@ -1937,7 +1891,19 @@ $(function() {
controlViewModel.requestData();
gcodeFilesViewModel.requestData();
timelapseViewModel.requestData();
settingsViewModel.requestData();
loginStateViewModel.subscribe(function(change, data) {
if ("login" == change) {
$("#gcode_upload").fileupload("enable");
settingsViewModel.requestData();
if (data.admin) {
usersViewModel.requestData();
}
} else {
$("#gcode_upload").fileupload("disable");
}
})
//~~ UI stuff

View File

@ -17,13 +17,15 @@
<link href="{{ url_for('static', filename='gcodeviewer/css/style.css') }}" rel="stylesheet" media="screen">
<script lang="javascript">
var AJAX_BASEURL = "/ajax/";
var AJAX_BASEURL = "{{ ajaxBaseUrl }}";
var CONFIG_GCODEFILESPERPAGE = 5;
var CONFIG_TIMELAPSEFILESPERPAGE = 10;
var CONFIG_USERSPERPAGE = 10;
var CONFIG_WEBCAM_STREAM = "{{ webcamStream }}";
var CONFIG_ACCESS_CONTROL = {% if enableAccessControl -%} true; {% else %} false; {%- endif %}
var WEB_SOCKET_SWF_LOCATION = "{{ url_for('static', filename='js/WebSocketMain.swf') }}";
var WEB_SOCKET_SWF_LOCATION = "{{ url_for('static', filename='js/socket.io/WebSocketMain.swf') }}";
var WEB_SOCKET_DEBUG = true;
</script>
<script src="{{ url_for('static', filename='js/less-1.3.3.min.js') }}" type="text/javascript"></script>
@ -35,13 +37,13 @@
<a class="brand" href="#"><img src="{{ url_for('static', filename='img/tentacle-20x20.png') }}"> <span data-bind="text: appearance.brand">OctoPrint</span></a>
<div class="nav-collapse">
<ul class="nav pull-right">
<li class="hide" data-bind="css: {hide: !loginState.isAdmin()}">
<li style="display: none;" data-bind="visible: loginState.isAdmin">
<a id="navbar_show_settings" class="pull-right" href="#settings_dialog">
<i class="icon-wrench"></i> Settings
</a>
</li>
{% if enableSystemMenu %}
<li class="dropdown hide" data-bind="css: {hide: !loginState.isAdmin()}">
<li class="dropdown" style="display: none" data-bind="visible: loginState.isAdmin">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<i class="icon-off"></i> System
<b class="caret"></b>
@ -68,6 +70,7 @@
<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="logout_button" data-bind="click: loginState.logout">Logout</a></li>
</ul>
</li>
@ -80,7 +83,7 @@
<div class="container octoprint-container">
<div class="row">
<div class="accordion span4">
<div class="accordion-group">
<div class="accordion-group" data-bind="visible: loginState.isUser" id="connection_accordion">
<div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" href="#connection"><i class="icon-signal"></i> Connection</a>
</div>
@ -97,7 +100,7 @@
</div>
</div>
</div>
<div class="accordion-group">
<div class="accordion-group" id="state_accordion">
<div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" href="#state"><i class="icon-info-sign"></i> State</a>
</div>
@ -124,8 +127,8 @@
</div>
</div>
</div>
<div class="accordion-group">
<div class="accordion-heading" id="files-heading">
<div class="accordion-group" id="files_accordion">
<div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" href="#files"><i class="icon-list"></i> Files</a>
<div class="settings-trigger btn-group">
@ -156,7 +159,7 @@
<td class="gcode_files_name" data-bind="text: name"></td>
<td class="gcode_files_size" data-bind="text: size"></td>
<td class="gcode_files_action">
<a href="#" class="icon-trash" title="Remove" data-bind="click: function() { $root.removeFile($data.name); }"></a>&nbsp;|&nbsp;<a href="#" class="icon-folder-open" title="Load" data-bind="click: function() { if ($root.isLoadActionPossible()) { $root.loadFile($data.name, false); } else { return; } }, css: {disabled: !$root.isLoadActionPossible()}"></a>&nbsp;|&nbsp;<a href="#" class="icon-print" title="Load and Print" data-bind="click: function() { if ($root.isLoadAndPrintActionPossible()) { $root.loadFile($data.name, true); } else { return; } }, css: {disabled: !$root.isLoadAndPrintActionPossible()}"></a>
<a href="#" class="icon-trash" title="Remove" data-bind="click: function() { if ($root.loginState.isUser()) { $root.removeFile($data.name); } else { return; } }, css: {disabled: !$root.loginState.isUser()}"></a>&nbsp;|&nbsp;<a href="#" class="icon-folder-open" title="Load" data-bind="click: function() { if ($root.isLoadActionPossible()) { $root.loadFile($data.name, false); } else { return; } }, css: {disabled: !$root.isLoadActionPossible()}"></a>&nbsp;|&nbsp;<a href="#" class="icon-print" title="Load and Print" data-bind="click: function() { if ($root.isLoadAndPrintActionPossible()) { $root.loadFile($data.name, true); } else { return; } }, css: {disabled: !$root.isLoadAndPrintActionPossible()}"></a>
</td>
</tr>
</tbody>
@ -172,16 +175,18 @@
<li data-bind="css: {disabled: listHelper.currentPage() === listHelper.lastPage()}"><a href="#" data-bind="click: listHelper.nextPage">»</a></li>
</ul>
</div>
<span class="btn btn-primary btn-block fileinput-button" style="margin-bottom: 10px">
<i class="icon-upload icon-white"></i>
<span>Upload</span>
<input id="gcode_upload" type="file" name="gcode_file" class="fileinput-button" data-url="/ajax/gcodefiles/upload">
</span>
<div id="gcode_upload_progress" class="progress" style="width: 100%;">
<div class="bar" style="width: 0%"></div>
</div>
<div>
<small>Hint: You can also drag and drop files on this page to upload them.</small>
<div style="display: none;" data-bind="visible: loginState.isUser">
<span class="btn btn-primary btn-block fileinput-button" data-bind="css: {disabled: !$root.loginState.isUser()}" style="margin-bottom: 10px">
<i class="icon-upload icon-white"></i>
<span>Upload</span>
<input id="gcode_upload" type="file" name="gcode_file" class="fileinput-button" data-url="/ajax/gcodefiles/upload" data-bind="enable: loginState.isUser()">
</span>
<div id="gcode_upload_progress" class="progress" style="width: 100%;">
<div class="bar" style="width: 0%"></div>
</div>
<div>
<small>Hint: You can also drag and drop files on this page to upload them.</small>
</div>
</div>
</div>
</div>
@ -193,7 +198,6 @@
<li class="active"><a href="#temp" data-toggle="tab">Temperature</a></li>
<li><a href="#control" data-toggle="tab">Control</a></li>
{% if enableGCodeVisualizer %}<li><a href="#gcode" data-toggle="tab">GCode Viewer</a></li>{% endif %}
<!--<li><a href="#speed" data-toggle="tab">Speed</a></li>-->
<li><a href="#term" data-toggle="tab">Terminal</a></li>
{% if enableTimelapse %}<li><a href="#timelapse" data-toggle="tab">Timelapse</a></li>{% endif %}
</ul>
@ -211,27 +215,29 @@
<label>Target: <strong data-bind="html: targetTempString"></strong></label>
<label for="temp_newTemp">New Target</label>
<div class="input-append">
<input type="text" id="temp_newTemp" data-bind="attr: {placeholder: targetTemp}, enable: isOperational() && loginState.isUser()" class="tempInput">
<span class="add-on">&deg;C</span>
</div>
<div class="btn-group">
<button type="submit" class="btn" id="temp_newTemp_set" data-bind="enable: isOperational() && loginState.isUser()">Set</button>
<button class="btn dropdown-toggle" data-toggle="dropdown" data-bind="enable: isOperational() && loginState.isUser()">
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<!-- ko foreach: temperature_profiles -->
<li>
<a href="#" data-bind="click: $parent.setTempFromProfile, text: 'Set ' + name + ' (' + extruder + '&deg;C)'"></a>
</li>
<!-- /ko -->
<li class="divider"></li>
<li>
<a href="#" data-bind="click: function() { $root.setTemp(0); }">Off</a>
</li>
</ul>
<div style="display: none;" data-bind="visible: loginState.isUser">
<label for="temp_newTemp">New Target</label>
<div class="input-append">
<input type="text" id="temp_newTemp" data-bind="attr: {placeholder: targetTemp}, enable: isOperational() && loginState.isUser()" class="tempInput">
<span class="add-on">&deg;C</span>
</div>
<div class="btn-group">
<button type="submit" class="btn" id="temp_newTemp_set" data-bind="enable: isOperational() && loginState.isUser()">Set</button>
<button class="btn dropdown-toggle" data-toggle="dropdown" data-bind="enable: isOperational() && loginState.isUser()">
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<!-- ko foreach: temperature_profiles -->
<li>
<a href="#" data-bind="click: $parent.setTempFromProfile, text: 'Set ' + name + ' (' + extruder + '&deg;C)'"></a>
</li>
<!-- /ko -->
<li class="divider"></li>
<li>
<a href="#" data-bind="click: function() { $root.setTemp(0); }">Off</a>
</li>
</ul>
</div>
</div>
</div>
<div class="form-horizontal span6">
@ -241,27 +247,29 @@
<label>Target: <strong data-bind="html: bedTargetTempString"></strong></label>
<label for="temp_newBedTemp">New Target</label>
<div class="input-append">
<input type="text" id="temp_newBedTemp" data-bind="attr: {placeholder: bedTargetTemp}, enable: isOperational() && loginState.isUser()" class="tempInput">
<span class="add-on">&deg;C</span>
</div>
<div class="btn-group">
<button type="submit" class="btn" id="temp_newBedTemp_set" data-bind="enable: isOperational() && loginState.isUser()">Set</button>
<button class="btn dropdown-toggle" data-toggle="dropdown" data-bind="enable: isOperational() && loginState.isUser()">
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<!-- ko foreach: temperature_profiles -->
<li>
<a href="#" data-bind="click: $parent.setBedTempFromProfile, text: 'Set ' + name + ' (' + bed + '&deg;C)'"></a>
</li>
<!-- /ko -->
<li class="divider"></li>
<li>
<a href="#" data-bind="click: function(){ $root.setBedTemp(0); }">Off</a>
</li>
</ul>
<div style="display: none;" data-bind="visible: loginState.isUser">
<label for="temp_newBedTemp">New Target</label>
<div class="input-append">
<input type="text" id="temp_newBedTemp" data-bind="attr: {placeholder: bedTargetTemp}, enable: isOperational() && loginState.isUser()" class="tempInput">
<span class="add-on">&deg;C</span>
</div>
<div class="btn-group">
<button type="submit" class="btn" id="temp_newBedTemp_set" data-bind="enable: isOperational() && loginState.isUser()">Set</button>
<button class="btn dropdown-toggle" data-toggle="dropdown" data-bind="enable: isOperational() && loginState.isUser()">
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<!-- ko foreach: temperature_profiles -->
<li>
<a href="#" data-bind="click: $parent.setBedTempFromProfile, text: 'Set ' + name + ' (' + bed + '&deg;C)'"></a>
</li>
<!-- /ko -->
<li class="divider"></li>
<li>
<a href="#" data-bind="click: function(){ $root.setBedTemp(0); }">Off</a>
</li>
</ul>
</div>
</div>
</div>
</div>
@ -273,7 +281,7 @@
</div>
{% endif %}
<div class="jog-panel">
<div class="jog-panel" style="display: none;" data-bind="visible: loginState.isUser">
<!-- XY jogging control panel -->
<div class="jog-panel">
<h1>X/Y</h1>
@ -313,7 +321,7 @@
</div>
</div>
<!-- Extrusion control panel -->
<div class="jog-panel">
<div class="jog-panel" style="display: none;" data-bind="visible: loginState.isUser">
<h1>E</h1>
<div>
<div class="input-append control-box">
@ -325,7 +333,7 @@
</div>
</div>
<!-- General control panel -->
<div class="jog-panel">
<div class="jog-panel" style="display: none;" data-bind="visible: loginState.isUser">
<h1>General</h1>
<div>
<button class="btn control-box" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendCustomCommand({type:'command',command:'M18'}) }"><i class="icon-off"></i>&nbsp;Motors off</button>
@ -335,7 +343,7 @@
</div>
<!-- Container for custom controls -->
<div style="clear: both;" data-bind="template: { name: $root.displayMode, foreach: controls }"></div>
<div style="clear: both; display: none;" data-bind="visible: loginState.isUser, template: { name: $root.displayMode, foreach: controls }"></div>
<!-- Templates for custom controls -->
<script type="text/html" id="customControls_sectionTemplate">
@ -360,37 +368,6 @@
<script type="text/html" id="customControls_emptyTemplate"><div></div></script>
<!-- End of templates for custom controls -->
</div>
<div class="tab-pane" id="speed">
<div class="form-horizontal" style="margin-bottom: 20px">
<label for="speed_outerWall">Outer Wall</label>
<div class="input-append">
<input type="text" id="speed_outerWall" class="input-mini" data-bind="enable: isOperational() && loginState.isUser(), attr: {placeholder: outerWall}">
<span class="add-on">%</span>
<button type="submit" class="btn" id="speed_outerWall_set" data-bind="enable: isOperational() && loginState.isUser()">Set</button>
</div>
<label for="speed_innerWall">Inner Wall</label>
<div class="input-append">
<input type="text" id="speed_innerWall" class="input-mini" data-bind="enable: isOperational() && loginState.isUser(), attr: {placeholder: innerWall}">
<span class="add-on">%</span>
<button type="submit" class="btn" id="speed_innerWall_set" data-bind="enable: isOperational() && loginState.isUser()">Set</button>
</div>
<label for="speed_fill">Fill</label>
<div class="input-append">
<input type="text" id="speed_fill" class="input-mini" data-bind="enable: isOperational() && loginState.isUser(), attr: {placeholder: fill}">
<span class="add-on">%</span>
<button type="submit" class="btn" id="speed_fill_set" data-bind="enable: isOperational() && loginState.isUser()">Set</button>
</div>
<label for="speed_support">Support</label>
<div class="input-append">
<input type="text" id="speed_support" class="input-mini" data-bind="enable: isOperational() && loginState.isUser(), attr: {placeholder: support}">
<span class="add-on">%</span>
<button type="submit" class="btn" id="speed_support_set" data-bind="enable: isOperational() && loginState.isUser()">Set</button>
</div>
</div>
</div>
<div class="tab-pane" id="gcode">
<canvas id="canvas" width="572" height="588"></canvas>
<div id="slider-vertical"></div>
@ -484,32 +461,34 @@
<input type="checkbox" id="terminal-autoscroll" data-bind="checked: autoscrollEnabled"> Autoscroll
</label>
<div class="input-append">
<input type="text" id="terminal-command">
<div class="input-append" style="display: none;" data-bind="visible: loginState.isUser">
<input type="text" id="terminal-command" data-bind="enable: isOperational() && loginState.isUser()">
<button class="btn" type="button" id="terminal-send" data-bind="enable: isOperational() && loginState.isUser()">Send</button>
</div>
</div>
{% if enableTimelapse %}
<div class="tab-pane" id="timelapse">
<h1>Timelapse Configuration</h1>
<div style="display: none;" data-bind="visible: loginState.isUser">
<h1>Timelapse Configuration</h1>
<label for="webcam_timelapse_mode">Timelapse Mode</label>
<select id="webcam_timelapse_mode" data-bind="value: timelapseType, enable: isOperational() && !isPrinting() && loginState.isUser()">
<option value="off">Off</option>
<option value="zchange">On Z Change</option>
<option value="timed">Timed</option>
</select>
<label for="webcam_timelapse_mode">Timelapse Mode</label>
<select id="webcam_timelapse_mode" data-bind="value: timelapseType, enable: isOperational() && !isPrinting() && loginState.isUser()">
<option value="off">Off</option>
<option value="zchange">On Z Change</option>
<option value="timed">Timed</option>
</select>
<div id="webcam_timelapse_timedsettings" data-bind="visible: intervalInputEnabled()">
<label for="webcam_timelapse_interval">Interval</label>
<div class="input-append">
<input type="text" class="input-mini" id="webcam_timelapse_interval" data-bind="value: timelapseTimedInterval, enable: isOperational() && !isPrinting()">
<span class="add-on">sec</span>
<div id="webcam_timelapse_timedsettings" data-bind="visible: intervalInputEnabled()">
<label for="webcam_timelapse_interval">Interval</label>
<div class="input-append">
<input type="text" class="input-mini" id="webcam_timelapse_interval" data-bind="value: timelapseTimedInterval, enable: isOperational() && !isPrinting() && loginState.isUser()">
<span class="add-on">sec</span>
</div>
</div>
</div>
<div>
<button class="btn" data-bind="click: save, enable: isOperational() && !isPrinting() && loginState.isUser()">Save Settings</button>
<div>
<button class="btn" data-bind="click: save, enable: isOperational() && !isPrinting() && loginState.isUser()">Save Settings</button>
</div>
</div>
<h1>Finished Timelapses</h1>
@ -529,7 +508,7 @@
<tr data-bind="attr: {title: name}">
<td class="timelapse_files_name" data-bind="text: name"></td>
<td class="timelapse_files_size" data-bind="text: size"></td>
<td class="timelapse_files_action"><a href="#" class="icon-trash" data-bind="click: $parent.removeFile"></a>&nbsp;|&nbsp;<a href="#" class="icon-download" data-bind="attr: {href: url}"></a></td>
<td class="timelapse_files_action"><a href="#" class="icon-trash" data-bind="click: function() { if ($root.loginState.isUser()) { $parent.removeFile(); } else { return; } }, css: {disabled: !$root.loginState.isUser()}"></a>&nbsp;|&nbsp;<a href="#" class="icon-download" data-bind="attr: {href: url}"></a></td>
</tr>
</tbody>
</table>
@ -559,26 +538,27 @@
</div>
</div>
{% include 'settings.html' %}
{% include 'dialogs.html' %}
{% include 'settings.jinja2' %}
{% include 'dialogs.jinja2' %}
<script type="text/javascript" src="http://code.jquery.com/jquery-latest.js"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/modernizr.custom.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/underscore-min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/knockout-2.2.1.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/bootstrap.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/bootstrap-modalmanager.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/bootstrap-modal.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery.ui.core.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery.ui.widget.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery.ui.mouse.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery.ui.slider.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery.pnotify.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery.flot.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery.iframe-transport.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery.fileupload.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/socket.io.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/underscore.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/knockout.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/bootstrap/bootstrap.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/bootstrap/bootstrap-modalmanager.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/bootstrap/bootstrap-modal.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery/jquery.ui.core.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery/jquery.ui.widget.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery/jquery.ui.mouse.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery/jquery.ui.slider.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery/jquery.pnotify.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery/jquery.flot.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery/jquery.iframe-transport.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery/jquery.fileupload.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/socket.io/socket.io.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/ui.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='gcodeviewer/js/ui.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='gcodeviewer/js/gCodeReader.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='gcodeviewer/js/renderer.js') }}"></script>

View File

@ -12,7 +12,7 @@
<li><a href="#settings_folder" data-toggle="tab">Folder</a></li>
<li><a href="#settings_temperature" data-toggle="tab">Temperature</a></li>
<li><a href="#settings_appearance" data-toggle="tab">Appearance</a></li>
<li><a href="#settings_users" data-toggle="tab">Users</a></li>
{% if enableAccessControl %}<li><a href="#settings_users" data-toggle="tab">Users</a></li>{% endif %}
</ul>
<div class="tab-content span8">
@ -185,6 +185,8 @@
</div>
</form>
</div>
{% if enableAccessControl %}
<div class="tab-pane" id="settings_users">
<table class="table table-condensed table-hover" id="system_users">
<thead>
@ -327,6 +329,8 @@
</div>
</div>
</div>
{% endif %}
</div>
</div>
</div>

View File

@ -65,6 +65,8 @@ class FilebasedUserManager(UserManager):
for name in data.keys():
attributes = data[name]
self._users[name] = User(name, attributes["password"], attributes["active"], attributes["roles"])
else:
self._users["admin"] = User("admin", "7557160613d5258f883014a7c3c0428de53040fc152b1791f1cc04a62b428c0c2a9c46ed330cdce9689353ab7a5352ba2b2ceb459b96e9c8ed7d0cb0b2c0c076", True, ["user", "admin"])
def _save(self, force=False):
if not self._dirty and not force:
@ -193,7 +195,8 @@ class User(UserMixin):
return {
"name": self._username,
"active": self.is_active(),
"admin": self.is_admin()
"admin": self.is_admin(),
"user": self.is_user()
}
def check_password(self, passwordHash):