Merge branch 'devel' into repetier

Conflicts:
	octoprint/util/comm.py
master
Gina Häußge 2013-04-16 15:14:18 +02:00
commit d721e5edc2
33 changed files with 2334 additions and 425 deletions

View File

@ -2,9 +2,11 @@
__author__ = "Gina Häußge <osd@foosel.net>" __author__ = "Gina Häußge <osd@foosel.net>"
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html' __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
from flask import Flask, request, render_template, jsonify, send_from_directory, abort, url_for
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
import tornadio2 import tornadio2
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
import os import os
import threading import threading
@ -12,23 +14,30 @@ import logging, logging.config
import subprocess import subprocess
from octoprint.printer import Printer, getConnectionOptions from octoprint.printer import Printer, getConnectionOptions
from octoprint.settings import settings from octoprint.settings import settings, valid_boolean_trues
import octoprint.timelapse as timelapse import octoprint.timelapse as timelapse
import octoprint.gcodefiles as gcodefiles import octoprint.gcodefiles as gcodefiles
import octoprint.util as util import octoprint.util as util
import octoprint.users as users
SUCCESS = {} SUCCESS = {}
BASEURL = "/ajax/" BASEURL = "/ajax/"
app = Flask("octoprint") app = Flask("octoprint")
# Only instantiated by the Server().run() method # Only instantiated by the Server().run() method
# In order that threads don't start too early when running as a Daemon # In order that threads don't start too early when running as a Daemon
printer = None printer = None
gcodeManager = None gcodeManager = None
userManager = None
principals = Principal(app)
admin_permission = Permission(RoleNeed("admin"))
user_permission = Permission(RoleNeed("user"))
#~~ Printer state #~~ Printer state
class PrinterStateConnection(tornadio2.SocketConnection): class PrinterStateConnection(tornadio2.SocketConnection):
def __init__(self, session, endpoint=None): def __init__(self, printer, gcodeManager, userManager, session, endpoint=None):
tornadio2.SocketConnection.__init__(self, session, endpoint) tornadio2.SocketConnection.__init__(self, session, endpoint)
self._logger = logging.getLogger(__name__) self._logger = logging.getLogger(__name__)
@ -40,6 +49,10 @@ class PrinterStateConnection(tornadio2.SocketConnection):
self._messageBacklog = [] self._messageBacklog = []
self._messageBacklogMutex = threading.Lock() self._messageBacklogMutex = threading.Lock()
self._printer = printer
self._gcodeManager = gcodeManager
self._userManager = userManager
def on_open(self, info): def on_open(self, info):
self._logger.info("New connection from client") self._logger.info("New connection from client")
# Use of global here is smelly # Use of global here is smelly
@ -99,21 +112,25 @@ class PrinterStateConnection(tornadio2.SocketConnection):
@app.route("/") @app.route("/")
def index(): def index():
return render_template( return render_template(
"index.html", "index.jinja2",
ajaxBaseUrl=BASEURL,
webcamStream=settings().get(["webcam", "stream"]), webcamStream=settings().get(["webcam", "stream"]),
enableTimelapse=(settings().get(["webcam", "snapshot"]) is not None and settings().get(["webcam", "ffmpeg"]) is not None), enableTimelapse=(settings().get(["webcam", "snapshot"]) is not None and settings().get(["webcam", "ffmpeg"]) is not None),
enableGCodeVisualizer=settings().get(["feature", "gCodeVisualizer"]), enableGCodeVisualizer=settings().get(["feature", "gCodeVisualizer"]),
enableSystemMenu=settings().get(["system"]) is not None and settings().get(["system", "actions"]) is not None and len(settings().get(["system", "actions"])) > 0 enableSystemMenu=settings().get(["system"]) is not None and settings().get(["system", "actions"]) is not None and len(settings().get(["system", "actions"])) > 0,
enableAccessControl=userManager is not None
) )
#~~ Printer control #~~ Printer control
@app.route(BASEURL + "control/connectionOptions", methods=["GET"]) @app.route(BASEURL + "control/connection/options", methods=["GET"])
def connectionOptions(): def connectionOptions():
return jsonify(getConnectionOptions()) return jsonify(getConnectionOptions())
@app.route(BASEURL + "control/connect", methods=["POST"]) @app.route(BASEURL + "control/connection", methods=["POST"])
@login_required
def connect(): def connect():
if "command" in request.values.keys() and request.values["command"] == "connect":
port = None port = None
baudrate = None baudrate = None
if "port" in request.values.keys(): if "port" in request.values.keys():
@ -125,14 +142,13 @@ def connect():
settings().setInt(["serial", "baudrate"], baudrate) settings().setInt(["serial", "baudrate"], baudrate)
settings().save() settings().save()
printer.connect(port=port, baudrate=baudrate) printer.connect(port=port, baudrate=baudrate)
return jsonify(state="Connecting") elif "command" in request.values.keys() and request.values["command"] == "disconnect":
@app.route(BASEURL + "control/disconnect", methods=["POST"])
def disconnect():
printer.disconnect() printer.disconnect()
return jsonify(state="Offline")
return jsonify(SUCCESS)
@app.route(BASEURL + "control/command", methods=["POST"]) @app.route(BASEURL + "control/command", methods=["POST"])
@login_required
def printerCommand(): def printerCommand():
if "application/json" in request.headers["Content-Type"]: if "application/json" in request.headers["Content-Type"]:
data = request.json data = request.json
@ -155,32 +171,27 @@ def printerCommand():
return jsonify(SUCCESS) return jsonify(SUCCESS)
@app.route(BASEURL + "control/print", methods=["POST"]) @app.route(BASEURL + "control/job", methods=["POST"])
def printGcode(): @login_required
def printJobControl():
if "command" in request.values.keys():
if request.values["command"] == "start":
printer.startPrint() printer.startPrint()
return jsonify(SUCCESS) elif request.values["command"] == "pause":
@app.route(BASEURL + "control/pause", methods=["POST"])
def pausePrint():
printer.togglePausePrint() printer.togglePausePrint()
return jsonify(SUCCESS) elif request.values["command"] == "cancel":
@app.route(BASEURL + "control/cancel", methods=["POST"])
def cancelPrint():
printer.cancelPrint() printer.cancelPrint()
return jsonify(SUCCESS) return jsonify(SUCCESS)
@app.route(BASEURL + "control/temperature", methods=["POST"]) @app.route(BASEURL + "control/temperature", methods=["POST"])
@login_required
def setTargetTemperature(): def setTargetTemperature():
if not printer.isOperational(): if "temp" in request.values.keys():
return jsonify(SUCCESS)
elif request.values.has_key("temp"):
# set target temperature # set target temperature
temp = request.values["temp"] temp = request.values["temp"]
printer.command("M104 S" + temp) printer.command("M104 S" + temp)
elif request.values.has_key("bedTemp"): if "bedTemp" in request.values.keys():
# set target bed temperature # set target bed temperature
bedTemp = request.values["bedTemp"] bedTemp = request.values["bedTemp"]
printer.command("M140 S" + bedTemp) printer.command("M140 S" + bedTemp)
@ -188,6 +199,7 @@ def setTargetTemperature():
return jsonify(SUCCESS) return jsonify(SUCCESS)
@app.route(BASEURL + "control/jog", methods=["POST"]) @app.route(BASEURL + "control/jog", methods=["POST"])
@login_required
def jog(): def jog():
if not printer.isOperational() or printer.isPrinting(): if not printer.isOperational() or printer.isPrinting():
# do not jog when a print job is running or we don't have a connection # do not jog when a print job is running or we don't have a connection
@ -224,6 +236,7 @@ def getSpeedValues():
return jsonify(feedrate=printer.feedrateState()) return jsonify(feedrate=printer.feedrateState())
@app.route(BASEURL + "control/speed", methods=["POST"]) @app.route(BASEURL + "control/speed", methods=["POST"])
@login_required
def speed(): def speed():
if not printer.isOperational(): if not printer.isOperational():
return jsonify(SUCCESS) return jsonify(SUCCESS)
@ -251,6 +264,7 @@ def readGcodeFile(filename):
return send_from_directory(settings().getBaseFolder("uploads"), filename, as_attachment=True) return send_from_directory(settings().getBaseFolder("uploads"), filename, as_attachment=True)
@app.route(BASEURL + "gcodefiles/upload", methods=["POST"]) @app.route(BASEURL + "gcodefiles/upload", methods=["POST"])
@login_required
def uploadGcodeFile(): def uploadGcodeFile():
filename = None filename = None
if "gcode_file" in request.files.keys(): if "gcode_file" in request.files.keys():
@ -259,10 +273,11 @@ def uploadGcodeFile():
return jsonify(files=gcodeManager.getAllFileData(), filename=filename) return jsonify(files=gcodeManager.getAllFileData(), filename=filename)
@app.route(BASEURL + "gcodefiles/load", methods=["POST"]) @app.route(BASEURL + "gcodefiles/load", methods=["POST"])
@login_required
def loadGcodeFile(): def loadGcodeFile():
if "filename" in request.values.keys(): if "filename" in request.values.keys():
printAfterLoading = False printAfterLoading = False
if "print" in request.values.keys() and request.values["print"]: if "print" in request.values.keys() and request.values["print"] in valid_boolean_trues:
printAfterLoading = True printAfterLoading = True
filename = gcodeManager.getAbsolutePath(request.values["filename"]) filename = gcodeManager.getAbsolutePath(request.values["filename"])
if filename is not None: if filename is not None:
@ -270,6 +285,7 @@ def loadGcodeFile():
return jsonify(SUCCESS) return jsonify(SUCCESS)
@app.route(BASEURL + "gcodefiles/delete", methods=["POST"]) @app.route(BASEURL + "gcodefiles/delete", methods=["POST"])
@login_required
def deleteGcodeFile(): def deleteGcodeFile():
if "filename" in request.values.keys(): if "filename" in request.values.keys():
filename = request.values["filename"] filename = request.values["filename"]
@ -308,6 +324,7 @@ def downloadTimelapse(filename):
return send_from_directory(settings().getBaseFolder("timelapse"), filename, as_attachment=True) return send_from_directory(settings().getBaseFolder("timelapse"), filename, as_attachment=True)
@app.route(BASEURL + "timelapse/<filename>", methods=["DELETE"]) @app.route(BASEURL + "timelapse/<filename>", methods=["DELETE"])
@login_required
def deleteTimelapse(filename): def deleteTimelapse(filename):
if util.isAllowedFile(filename, set(["mpg"])): if util.isAllowedFile(filename, set(["mpg"])):
secure = os.path.join(settings().getBaseFolder("timelapse"), secure_filename(filename)) secure = os.path.join(settings().getBaseFolder("timelapse"), secure_filename(filename))
@ -315,7 +332,8 @@ def deleteTimelapse(filename):
os.remove(secure) os.remove(secure)
return getTimelapseData() return getTimelapseData()
@app.route(BASEURL + "timelapse/config", methods=["POST"]) @app.route(BASEURL + "timelapse", methods=["POST"])
@login_required
def setTimelapseConfig(): def setTimelapseConfig():
if request.values.has_key("type"): if request.values.has_key("type"):
type = request.values["type"] type = request.values["type"]
@ -381,6 +399,8 @@ def getSettings():
}) })
@app.route(BASEURL + "settings", methods=["POST"]) @app.route(BASEURL + "settings", methods=["POST"])
@login_required
@admin_permission.require(403)
def setSettings(): def setSettings():
if "application/json" in request.headers["Content-Type"]: if "application/json" in request.headers["Content-Type"]:
data = request.json data = request.json
@ -425,9 +445,117 @@ def setSettings():
return getSettings() return getSettings()
#~~ user settings
@app.route(BASEURL + "users", methods=["GET"])
@login_required
@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(403)
def addUser():
if userManager is None:
return jsonify(SUCCESS)
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:
abort(409)
return getUsers()
@app.route(BASEURL + "users/<username>", methods=["GET"])
@login_required
def getUser(username):
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:
abort(403)
@app.route(BASEURL + "users/<username>", methods=["PUT"])
@login_required
@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"]:
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:
abort(404)
@app.route(BASEURL + "users/<username>", methods=["DELETE"])
@login_required
@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:
abort(404)
@app.route(BASEURL + "users/<username>/password", methods=["PUT"])
@login_required
def changePasswordForUser(username):
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 #~~ system control
@app.route(BASEURL + "system", methods=["POST"]) @app.route(BASEURL + "system", methods=["POST"])
@login_required
@admin_permission.require(403)
def performSystemAction(): def performSystemAction():
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
if request.values.has_key("action"): if request.values.has_key("action"):
@ -446,6 +574,61 @@ def performSystemAction():
return app.make_response(("Command failed: %r" % ex, 500, [])) return app.make_response(("Command failed: %r" % ex, 500, []))
return jsonify(SUCCESS) return jsonify(SUCCESS)
#~~ Login/user handling
@app.route(BASEURL + "login", methods=["POST"])
def login():
if userManager is not None and "user" in request.values.keys() and "pass" in request.values.keys():
username = request.values["user"]
password = request.values["pass"]
if "remember" in request.values.keys() and request.values["remember"]:
remember = True
else:
remember = False
user = userManager.findUser(username)
if user is not None:
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(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(user.asDict())
return jsonify(SUCCESS)
@app.route(BASEURL + "logout", methods=["POST"])
@login_required
def logout():
# Remove session keys set by Flask-Principal
for key in ('identity.name', 'identity.auth_type'):
del session[key]
identity_changed.send(current_app._get_current_object(), identity=AnonymousIdentity())
logout_user()
return jsonify(SUCCESS)
@identity_loaded.connect_via(app)
def on_identity_loaded(sender, identity):
user = load_user(identity.id)
if user is None:
return
identity.provides.add(UserNeed(user.get_name()))
if user.is_user():
identity.provides.add(RoleNeed("user"))
if user.is_admin():
identity.provides.add(RoleNeed("admin"))
def load_user(id):
if userManager is not None:
return userManager.findUser(id)
return users.DummyUser()
#~~ startup code #~~ startup code
class Server(): class Server():
def __init__(self, configfile=None, basedir=None, host="0.0.0.0", port=5000, debug=False): def __init__(self, configfile=None, basedir=None, host="0.0.0.0", port=5000, debug=False):
@ -459,6 +642,7 @@ class Server():
# Global as I can't work out a way to get it into PrinterStateConnection # Global as I can't work out a way to get it into PrinterStateConnection
global printer global printer
global gcodeManager global gcodeManager
global userManager
from tornado.wsgi import WSGIContainer from tornado.wsgi import WSGIContainer
from tornado.httpserver import HTTPServer from tornado.httpserver import HTTPServer
@ -470,19 +654,37 @@ class Server():
# then initialize logging # then initialize logging
self._initLogging(self._debug) self._initLogging(self._debug)
logger = logging.getLogger(__name__)
gcodeManager = gcodefiles.GcodeManager() gcodeManager = gcodefiles.GcodeManager()
printer = Printer(gcodeManager) printer = Printer(gcodeManager)
if settings().getBoolean(["accessControl", "enabled"]):
userManagerName = settings().get(["accessControl", "userManager"])
try:
clazz = util.getClass(userManagerName)
userManager = clazz()
except AttributeError, e:
logger.exception("Could not instantiate user manager %s, will run with accessControl disabled!" % userManagerName)
app.secret_key = "k3PuVYgtxNm8DXKKTw2nWmFQQun9qceV"
login_manager = LoginManager()
login_manager.session_protection = "strong"
login_manager.user_callback = load_user
if userManager is None:
login_manager.anonymous_user = users.DummyUser
principals.identity_loaders.appendleft(users.dummy_identity_loader)
login_manager.init_app(app)
if self._host is None: if self._host is None:
self._host = settings().get(["server", "host"]) self._host = settings().get(["server", "host"])
if self._port is None: if self._port is None:
self._port = settings().getInt(["server", "port"]) self._port = settings().getInt(["server", "port"])
logging.getLogger(__name__).info("Listening on http://%s:%d" % (self._host, self._port)) logger.info("Listening on http://%s:%d" % (self._host, self._port))
app.debug = self._debug app.debug = self._debug
self._router = tornadio2.TornadioRouter(PrinterStateConnection) self._router = tornadio2.TornadioRouter(self._createSocketConnection)
self._tornado_app = Application(self._router.urls + [ self._tornado_app = Application(self._router.urls + [
(".*", FallbackHandler, {"fallback": WSGIContainer(app)}) (".*", FallbackHandler, {"fallback": WSGIContainer(app)})
@ -491,11 +693,15 @@ class Server():
self._server.listen(self._port, address=self._host) self._server.listen(self._port, address=self._host)
IOLoop.instance().start() IOLoop.instance().start()
def _createSocketConnection(self, session, endpoint=None):
global printer, gcodeManager, userManager
return PrinterStateConnection(printer, gcodeManager, userManager, session, endpoint)
def _initSettings(self, configfile, basedir): def _initSettings(self, configfile, basedir):
s = settings(init=True, basedir=basedir, configfile=configfile) s = settings(init=True, basedir=basedir, configfile=configfile)
def _initLogging(self, debug): def _initLogging(self, debug):
self._config = { config = {
"version": 1, "version": 1,
"formatters": { "formatters": {
"simple": { "simple": {
@ -534,13 +740,13 @@ class Server():
} }
if debug: if debug:
self._config["loggers"]["SERIAL"] = { config["loggers"]["SERIAL"] = {
"level": "DEBUG", "level": "DEBUG",
"handlers": ["serialFile"], "handlers": ["serialFile"],
"propagate": False "propagate": False
} }
logging.config.dictConfig(self._config) logging.config.dictConfig(config)
if __name__ == "__main__": if __name__ == "__main__":
octoprint = Server() octoprint = Server()

View File

@ -72,6 +72,11 @@ default_settings = {
"controls": [], "controls": [],
"system": { "system": {
"actions": [] "actions": []
},
"accessControl": {
"enabled": False,
"userManager": "octoprint.users.FilebasedUserManager",
"userfile": None
} }
} }

View File

@ -0,0 +1,214 @@
/*!
* Bootstrap Modal
*
* Copyright Jordan Schroter
* Licensed under the Apache License v2.0
* http://www.apache.org/licenses/LICENSE-2.0
*
*/
.modal-open {
overflow: hidden;
}
/* add a scroll bar to stop page from jerking around */
.modal-open.page-overflow .page-container,
.modal-open.page-overflow .page-container .navbar-fixed-top,
.modal-open.page-overflow .page-container .navbar-fixed-bottom,
.modal-open.page-overflow .modal-scrollable {
overflow-y: scroll;
}
@media (max-width: 979px) {
.modal-open.page-overflow .page-container .navbar-fixed-top,
.modal-open.page-overflow .page-container .navbar-fixed-bottom {
overflow-y: visible;
}
}
.modal-scrollable {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
overflow: auto;
}
.modal {
outline: none;
position: absolute;
margin-top: 0;
top: 50%;
overflow: visible; /* allow content to popup out (i.e tooltips) */
}
.modal.fade {
top: -100%;
-webkit-transition: opacity 0.3s linear, top 0.3s ease-out, bottom 0.3s ease-out, margin-top 0.3s ease-out;
-moz-transition: opacity 0.3s linear, top 0.3s ease-out, bottom 0.3s ease-out, margin-top 0.3s ease-out;
-o-transition: opacity 0.3s linear, top 0.3s ease-out, bottom 0.3s ease-out, margin-top 0.3s ease-out;
transition: opacity 0.3s linear, top 0.3s ease-out, bottom 0.3s ease-out, margin-top 0.3s ease-out;
}
.modal.fade.in {
top: 50%;
}
.modal-body {
max-height: none;
overflow: visible;
}
.modal.modal-absolute {
position: absolute;
z-index: 950;
}
.modal .loading-mask {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background: #fff;
border-radius: 6px;
}
.modal-backdrop.modal-absolute{
position: absolute;
z-index: 940;
}
.modal-backdrop,
.modal-backdrop.fade.in{
opacity: 0.7;
filter: alpha(opacity=70);
background: #fff;
}
.modal.container {
width: 940px;
margin-left: -470px;
}
/* Modal Overflow */
.modal-overflow.modal {
top: 1%;
}
.modal-overflow.modal.fade {
top: -100%;
}
.modal-overflow.modal.fade.in {
top: 1%;
}
.modal-overflow .modal-body {
overflow: auto;
-webkit-overflow-scrolling: touch;
}
/* Responsive */
@media (min-width: 1200px) {
.modal.container {
width: 1170px;
margin-left: -585px;
}
}
@media (max-width: 979px) {
.modal,
.modal.container,
.modal.modal-overflow {
top: 1%;
right: 1%;
left: 1%;
bottom: auto;
width: auto !important;
height: auto !important;
margin: 0 !important;
padding: 0 !important;
}
.modal.fade.in,
.modal.container.fade.in,
.modal.modal-overflow.fade.in {
top: 1%;
bottom: auto;
}
.modal-body,
.modal-overflow .modal-body {
position: static;
margin: 0;
height: auto !important;
max-height: none !important;
overflow: visible !important;
}
.modal-footer,
.modal-overflow .modal-footer {
position: static;
}
}
.loading-spinner {
position: absolute;
top: 50%;
left: 50%;
margin: -12px 0 0 -12px;
}
/*
Animate.css - http://daneden.me/animate
Licensed under the license (http://licence.visualidiot.com/)
Copyright (c) 2012 Dan Eden*/
.animated {
-webkit-animation-duration: 1s;
-moz-animation-duration: 1s;
-o-animation-duration: 1s;
animation-duration: 1s;
-webkit-animation-fill-mode: both;
-moz-animation-fill-mode: both;
-o-animation-fill-mode: both;
animation-fill-mode: both;
}
@-webkit-keyframes shake {
0%, 100% {-webkit-transform: translateX(0);}
10%, 30%, 50%, 70%, 90% {-webkit-transform: translateX(-10px);}
20%, 40%, 60%, 80% {-webkit-transform: translateX(10px);}
}
@-moz-keyframes shake {
0%, 100% {-moz-transform: translateX(0);}
10%, 30%, 50%, 70%, 90% {-moz-transform: translateX(-10px);}
20%, 40%, 60%, 80% {-moz-transform: translateX(10px);}
}
@-o-keyframes shake {
0%, 100% {-o-transform: translateX(0);}
10%, 30%, 50%, 70%, 90% {-o-transform: translateX(-10px);}
20%, 40%, 60%, 80% {-o-transform: translateX(10px);}
}
@keyframes shake {
0%, 100% {transform: translateX(0);}
10%, 30%, 50%, 70%, 90% {transform: translateX(-10px);}
20%, 40%, 60%, 80% {transform: translateX(10px);}
}
.shake {
-webkit-animation-name: shake;
-moz-animation-name: shake;
-o-animation-name: shake;
animation-name: shake;
}

View File

@ -113,7 +113,20 @@ body {
.accordion-heading { .accordion-heading {
.settings-trigger { .settings-trigger {
float: right; float: right;
padding: 0px 15px;
.dropdown-toggle {
display: inline-block;
padding: 8px 15px;
font-size: 14px;
line-height: 20px;
color: #000;
text-decoration: none;
background: none;
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
}
} }
a.accordion-toggle { a.accordion-toggle {
@ -181,6 +194,42 @@ table {
&.timelapse_files_action { &.timelapse_files_action {
text-align: center; text-align: center;
width: 45px; width: 45px;
a {
text-decoration: none;
color: #000;
&.disabled {
color: #ccc;
cursor: default;
}
}
}
// 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;
}
}
} }
} }
} }
@ -274,9 +323,9 @@ ul.dropdown-menu li a {
} }
} }
/** Controls */ /** Control tab */
#controls { #control {
overflow: hidden; overflow: hidden;
.jog-panel { .jog-panel {
@ -324,7 +373,6 @@ ul.dropdown-menu li a {
/** Settings dialog */ /** Settings dialog */
#settings_dialog { #settings_dialog {
width: 650px;
} }
/** Footer */ /** Footer */

View File

@ -239,7 +239,7 @@ GCODE.ui = (function(){
}); });
if(!Modernizr.canvas)fatal.push("<li>Your browser doesn't seem to support HTML5 Canvas, this application won't work without it.</li>"); if(!Modernizr.canvas)fatal.push("<li>Your browser doesn't seem to support HTML5 Canvas, this application won't work without it.</li>");
if(!Modernizr.filereader)fatal.push("<li>Your browser doesn't seem to support HTML5 File API, this application won't work without it.</li>"); //if(!Modernizr.filereader)fatal.push("<li>Your browser doesn't seem to support HTML5 File API, this application won't work without it.</li>");
if(!Modernizr.webworkers)fatal.push("<li>Your browser doesn't seem to support HTML5 Web Workers, this application won't work without it.</li>"); if(!Modernizr.webworkers)fatal.push("<li>Your browser doesn't seem to support HTML5 Web Workers, this application won't work without it.</li>");
if(!Modernizr.svg)fatal.push("<li>Your browser doesn't seem to support HTML5 SVG, this application won't work without it.</li>"); if(!Modernizr.svg)fatal.push("<li>Your browser doesn't seem to support HTML5 SVG, this application won't work without it.</li>");
@ -249,7 +249,7 @@ GCODE.ui = (function(){
return false; return false;
} }
if(!Modernizr.webgl){ if(!Modernizr.webgl && GCODE.renderer3d){
warnings.push("<li>Your browser doesn't seem to support HTML5 Web GL, 3d mode is not recommended, going to be SLOW!</li>"); warnings.push("<li>Your browser doesn't seem to support HTML5 Web GL, 3d mode is not recommended, going to be SLOW!</li>");
GCODE.renderer3d.setOption({rendererType: "canvas"}); GCODE.renderer3d.setOption({rendererType: "canvas"});
} }

View File

@ -0,0 +1,374 @@
/* ===========================================================
* bootstrap-modal.js v2.1
* ===========================================================
* Copyright 2012 Jordan Schroter
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ========================================================== */
!function ($) {
"use strict"; // jshint ;_;
/* MODAL CLASS DEFINITION
* ====================== */
var Modal = function (element, options) {
this.init(element, options);
};
Modal.prototype = {
constructor: Modal,
init: function (element, options) {
this.options = options;
this.$element = $(element)
.delegate('[data-dismiss="modal"]', 'click.dismiss.modal', $.proxy(this.hide, this));
this.options.remote && this.$element.find('.modal-body').load(this.options.remote);
var manager = typeof this.options.manager === 'function' ?
this.options.manager.call(this) : this.options.manager;
manager = manager.appendModal ?
manager : $(manager).modalmanager().data('modalmanager');
manager.appendModal(this);
},
toggle: function () {
return this[!this.isShown ? 'show' : 'hide']();
},
show: function () {
var e = $.Event('show');
if (this.isShown) return;
this.$element.trigger(e);
if (e.isDefaultPrevented()) return;
this.escape();
this.tab();
this.options.loading && this.loading();
},
hide: function (e) {
e && e.preventDefault();
e = $.Event('hide');
this.$element.trigger(e);
if (!this.isShown || e.isDefaultPrevented()) return (this.isShown = false);
this.isShown = false;
this.escape();
this.tab();
this.isLoading && this.loading();
$(document).off('focusin.modal');
this.$element
.removeClass('in')
.removeClass('animated')
.removeClass(this.options.attentionAnimation)
.removeClass('modal-overflow')
.attr('aria-hidden', true);
$.support.transition && this.$element.hasClass('fade') ?
this.hideWithTransition() :
this.hideModal();
},
layout: function () {
var prop = this.options.height ? 'height' : 'max-height',
value = this.options.height || this.options.maxHeight;
if (this.options.width){
this.$element.css('width', this.options.width);
var that = this;
this.$element.css('margin-left', function () {
if (/%/ig.test(that.options.width)){
return -(parseInt(that.options.width) / 2) + '%';
} else {
return -($(this).width() / 2) + 'px';
}
});
} else {
this.$element.css('width', '');
this.$element.css('margin-left', '');
}
this.$element.find('.modal-body')
.css('overflow', '')
.css(prop, '');
var modalOverflow = $(window).height() - 10 < this.$element.height();
if (value){
this.$element.find('.modal-body')
.css('overflow', 'auto')
.css(prop, value);
}
if (modalOverflow || this.options.modalOverflow) {
this.$element
.css('margin-top', 0)
.addClass('modal-overflow');
} else {
this.$element
.css('margin-top', 0 - this.$element.height() / 2)
.removeClass('modal-overflow');
}
},
tab: function () {
var that = this;
if (this.isShown && this.options.consumeTab) {
this.$element.on('keydown.tabindex.modal', '[data-tabindex]', function (e) {
if (e.keyCode && e.keyCode == 9){
var $next = $(this),
$rollover = $(this);
that.$element.find('[data-tabindex]:enabled:not([readonly])').each(function (e) {
if (!e.shiftKey){
$next = $next.data('tabindex') < $(this).data('tabindex') ?
$next = $(this) :
$rollover = $(this);
} else {
$next = $next.data('tabindex') > $(this).data('tabindex') ?
$next = $(this) :
$rollover = $(this);
}
});
$next[0] !== $(this)[0] ?
$next.focus() : $rollover.focus();
e.preventDefault();
}
});
} else if (!this.isShown) {
this.$element.off('keydown.tabindex.modal');
}
},
escape: function () {
var that = this;
if (this.isShown && this.options.keyboard) {
if (!this.$element.attr('tabindex')) this.$element.attr('tabindex', -1);
this.$element.on('keyup.dismiss.modal', function (e) {
e.which == 27 && that.hide();
});
} else if (!this.isShown) {
this.$element.off('keyup.dismiss.modal')
}
},
hideWithTransition: function () {
var that = this
, timeout = setTimeout(function () {
that.$element.off($.support.transition.end);
that.hideModal();
}, 500);
this.$element.one($.support.transition.end, function () {
clearTimeout(timeout);
that.hideModal();
});
},
hideModal: function () {
this.$element
.hide()
.trigger('hidden');
var prop = this.options.height ? 'height' : 'max-height';
var value = this.options.height || this.options.maxHeight;
if (value){
this.$element.find('.modal-body')
.css('overflow', '')
.css(prop, '');
}
},
removeLoading: function () {
this.$loading.remove();
this.$loading = null;
this.isLoading = false;
},
loading: function (callback) {
callback = callback || function () {};
var animate = this.$element.hasClass('fade') ? 'fade' : '';
if (!this.isLoading) {
var doAnimate = $.support.transition && animate;
this.$loading = $('<div class="loading-mask ' + animate + '">')
.append(this.options.spinner)
.appendTo(this.$element);
if (doAnimate) this.$loading[0].offsetWidth; // force reflow
this.$loading.addClass('in');
this.isLoading = true;
doAnimate ?
this.$loading.one($.support.transition.end, callback) :
callback();
} else if (this.isLoading && this.$loading) {
this.$loading.removeClass('in');
var that = this;
$.support.transition && this.$element.hasClass('fade')?
this.$loading.one($.support.transition.end, function () { that.removeLoading() }) :
that.removeLoading();
} else if (callback) {
callback(this.isLoading);
}
},
focus: function () {
var $focusElem = this.$element.find(this.options.focusOn);
$focusElem = $focusElem.length ? $focusElem : this.$element;
$focusElem.focus();
},
attention: function (){
// NOTE: transitionEnd with keyframes causes odd behaviour
if (this.options.attentionAnimation){
this.$element
.removeClass('animated')
.removeClass(this.options.attentionAnimation);
var that = this;
setTimeout(function () {
that.$element
.addClass('animated')
.addClass(that.options.attentionAnimation);
}, 0);
}
this.focus();
},
destroy: function () {
var e = $.Event('destroy');
this.$element.trigger(e);
if (e.isDefaultPrevented()) return;
this.teardown();
},
teardown: function () {
if (!this.$parent.length){
this.$element.remove();
this.$element = null;
return;
}
if (this.$parent !== this.$element.parent()){
this.$element.appendTo(this.$parent);
}
this.$element.off('.modal');
this.$element.removeData('modal');
this.$element
.removeClass('in')
.attr('aria-hidden', true);
}
};
/* MODAL PLUGIN DEFINITION
* ======================= */
$.fn.modal = function (option, args) {
return this.each(function () {
var $this = $(this),
data = $this.data('modal'),
options = $.extend({}, $.fn.modal.defaults, $this.data(), typeof option == 'object' && option);
if (!data) $this.data('modal', (data = new Modal(this, options)));
if (typeof option == 'string') data[option].apply(data, [].concat(args));
else if (options.show) data.show()
})
};
$.fn.modal.defaults = {
keyboard: true,
backdrop: true,
loading: false,
show: true,
width: null,
height: null,
maxHeight: null,
modalOverflow: false,
consumeTab: true,
focusOn: null,
replace: false,
resize: false,
attentionAnimation: 'shake',
manager: 'body',
spinner: '<div class="loading-spinner" style="width: 200px; margin-left: -100px;"><div class="progress progress-striped active"><div class="bar" style="width: 100%;"></div></div></div>'
};
$.fn.modal.Constructor = Modal;
/* MODAL DATA-API
* ============== */
$(function () {
$(document).off('click.modal').on('click.modal.data-api', '[data-toggle="modal"]', function ( e ) {
var $this = $(this),
href = $this.attr('href'),
$target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))), //strip for ie7
option = $target.data('modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data());
e.preventDefault();
$target
.modal(option)
.one('hide', function () {
$this.focus();
})
});
});
}(window.jQuery);

View File

@ -0,0 +1,412 @@
/* ===========================================================
* bootstrap-modalmanager.js v2.1
* ===========================================================
* Copyright 2012 Jordan Schroter.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ========================================================== */
!function ($) {
"use strict"; // jshint ;_;
/* MODAL MANAGER CLASS DEFINITION
* ====================== */
var ModalManager = function (element, options) {
this.init(element, options);
};
ModalManager.prototype = {
constructor: ModalManager,
init: function (element, options) {
this.$element = $(element);
this.options = $.extend({}, $.fn.modalmanager.defaults, this.$element.data(), typeof options == 'object' && options);
this.stack = [];
this.backdropCount = 0;
if (this.options.resize) {
var resizeTimeout,
that = this;
$(window).on('resize.modal', function(){
resizeTimeout && clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(function(){
for (var i = 0; i < that.stack.length; i++){
that.stack[i].isShown && that.stack[i].layout();
}
}, 10);
});
}
},
createModal: function (element, options) {
$(element).modal($.extend({ manager: this }, options));
},
appendModal: function (modal) {
this.stack.push(modal);
var that = this;
modal.$element.on('show.modalmanager', targetIsSelf(function (e) {
var showModal = function(){
modal.isShown = true;
var transition = $.support.transition && modal.$element.hasClass('fade');
that.$element
.toggleClass('modal-open', that.hasOpenModal())
.toggleClass('page-overflow', $(window).height() < that.$element.height());
modal.$parent = modal.$element.parent();
modal.$container = that.createContainer(modal);
modal.$element.appendTo(modal.$container);
that.backdrop(modal, function () {
modal.$element.show();
if (transition) {
//modal.$element[0].style.display = 'run-in';
modal.$element[0].offsetWidth;
//modal.$element.one($.support.transition.end, function () { modal.$element[0].style.display = 'block' });
}
modal.layout();
modal.$element
.addClass('in')
.attr('aria-hidden', false);
var complete = function () {
that.setFocus();
modal.$element.trigger('shown');
};
transition ?
modal.$element.one($.support.transition.end, complete) :
complete();
});
};
modal.options.replace ?
that.replace(showModal) :
showModal();
}));
modal.$element.on('hidden.modalmanager', targetIsSelf(function (e) {
that.backdrop(modal);
if (modal.$backdrop){
$.support.transition && modal.$element.hasClass('fade') ?
modal.$backdrop.one($.support.transition.end, function () { that.destroyModal(modal) }) :
that.destroyModal(modal);
} else {
that.destroyModal(modal);
}
}));
modal.$element.on('destroy.modalmanager', targetIsSelf(function (e) {
that.removeModal(modal);
}));
},
destroyModal: function (modal) {
modal.destroy();
var hasOpenModal = this.hasOpenModal();
this.$element.toggleClass('modal-open', hasOpenModal);
if (!hasOpenModal){
this.$element.removeClass('page-overflow');
}
this.removeContainer(modal);
this.setFocus();
},
hasOpenModal: function () {
for (var i = 0; i < this.stack.length; i++){
if (this.stack[i].isShown) return true;
}
return false;
},
setFocus: function () {
var topModal;
for (var i = 0; i < this.stack.length; i++){
if (this.stack[i].isShown) topModal = this.stack[i];
}
if (!topModal) return;
topModal.focus();
},
removeModal: function (modal) {
modal.$element.off('.modalmanager');
if (modal.$backdrop) this.removeBackdrop.call(modal);
this.stack.splice(this.getIndexOfModal(modal), 1);
},
getModalAt: function (index) {
return this.stack[index];
},
getIndexOfModal: function (modal) {
for (var i = 0; i < this.stack.length; i++){
if (modal === this.stack[i]) return i;
}
},
replace: function (callback) {
var topModal;
for (var i = 0; i < this.stack.length; i++){
if (this.stack[i].isShown) topModal = this.stack[i];
}
if (topModal) {
this.$backdropHandle = topModal.$backdrop;
topModal.$backdrop = null;
callback && topModal.$element.one('hidden',
targetIsSelf( $.proxy(callback, this) ));
topModal.hide();
} else if (callback) {
callback();
}
},
removeBackdrop: function (modal) {
modal.$backdrop.remove();
modal.$backdrop = null;
},
createBackdrop: function (animate) {
var $backdrop;
if (!this.$backdropHandle) {
$backdrop = $('<div class="modal-backdrop ' + animate + '" />')
.appendTo(this.$element);
} else {
$backdrop = this.$backdropHandle;
$backdrop.off('.modalmanager');
this.$backdropHandle = null;
this.isLoading && this.removeSpinner();
}
return $backdrop
},
removeContainer: function (modal) {
modal.$container.remove();
modal.$container = null;
},
createContainer: function (modal) {
var $container;
$container = $('<div class="modal-scrollable">')
.css('z-index', getzIndex( 'modal',
modal ? this.getIndexOfModal(modal) : this.stack.length ))
.appendTo(this.$element);
if (modal && modal.options.backdrop != 'static') {
$container.on('click.modal', targetIsSelf(function (e) {
modal.hide();
}));
} else if (modal) {
$container.on('click.modal', targetIsSelf(function (e) {
modal.attention();
}));
}
return $container;
},
backdrop: function (modal, callback) {
var animate = modal.$element.hasClass('fade') ? 'fade' : '',
showBackdrop = modal.options.backdrop &&
this.backdropCount < this.options.backdropLimit;
if (modal.isShown && showBackdrop) {
var doAnimate = $.support.transition && animate && !this.$backdropHandle;
modal.$backdrop = this.createBackdrop(animate);
modal.$backdrop.css('z-index', getzIndex( 'backdrop', this.getIndexOfModal(modal) ));
if (doAnimate) modal.$backdrop[0].offsetWidth; // force reflow
modal.$backdrop.addClass('in');
this.backdropCount += 1;
doAnimate ?
modal.$backdrop.one($.support.transition.end, callback) :
callback();
} else if (!modal.isShown && modal.$backdrop) {
modal.$backdrop.removeClass('in');
this.backdropCount -= 1;
var that = this;
$.support.transition && modal.$element.hasClass('fade')?
modal.$backdrop.one($.support.transition.end, function () { that.removeBackdrop(modal) }) :
that.removeBackdrop(modal);
} else if (callback) {
callback();
}
},
removeSpinner: function(){
this.$spinner && this.$spinner.remove();
this.$spinner = null;
this.isLoading = false;
},
removeLoading: function () {
this.$backdropHandle && this.$backdropHandle.remove();
this.$backdropHandle = null;
this.removeSpinner();
},
loading: function (callback) {
callback = callback || function () { };
this.$element
.toggleClass('modal-open', !this.isLoading || this.hasOpenModal())
.toggleClass('page-overflow', $(window).height() < this.$element.height());
if (!this.isLoading) {
this.$backdropHandle = this.createBackdrop('fade');
this.$backdropHandle[0].offsetWidth; // force reflow
this.$backdropHandle
.css('z-index', getzIndex('backdrop', this.stack.length))
.addClass('in');
var $spinner = $(this.options.spinner)
.css('z-index', getzIndex('modal', this.stack.length))
.appendTo(this.$element)
.addClass('in');
this.$spinner = $(this.createContainer())
.append($spinner)
.on('click.modalmanager', $.proxy(this.loading, this));
this.isLoading = true;
$.support.transition ?
this.$backdropHandle.one($.support.transition.end, callback) :
callback();
} else if (this.isLoading && this.$backdropHandle) {
this.$backdropHandle.removeClass('in');
var that = this;
$.support.transition ?
this.$backdropHandle.one($.support.transition.end, function () { that.removeLoading() }) :
that.removeLoading();
} else if (callback) {
callback(this.isLoading);
}
}
};
/* PRIVATE METHODS
* ======================= */
// computes and caches the zindexes
var getzIndex = (function () {
var zIndexFactor,
baseIndex = {};
return function (type, pos) {
if (typeof zIndexFactor === 'undefined'){
var $baseModal = $('<div class="modal hide" />').appendTo('body'),
$baseBackdrop = $('<div class="modal-backdrop hide" />').appendTo('body');
baseIndex['modal'] = +$baseModal.css('z-index');
baseIndex['backdrop'] = +$baseBackdrop.css('z-index');
zIndexFactor = baseIndex['modal'] - baseIndex['backdrop'];
$baseModal.remove();
$baseBackdrop.remove();
$baseBackdrop = $baseModal = null;
}
return baseIndex[type] + (zIndexFactor * pos);
}
}());
// make sure the event target is the modal itself in order to prevent
// other components such as tabsfrom triggering the modal manager.
// if Boostsrap namespaced events, this would not be needed.
function targetIsSelf(callback){
return function (e) {
if (this === e.target){
return callback.apply(this, arguments);
}
}
}
/* MODAL MANAGER PLUGIN DEFINITION
* ======================= */
$.fn.modalmanager = function (option, args) {
return this.each(function () {
var $this = $(this),
data = $this.data('modalmanager');
if (!data) $this.data('modalmanager', (data = new ModalManager(this, option)));
if (typeof option === 'string') data[option].apply(data, [].concat(args))
})
};
$.fn.modalmanager.defaults = {
backdropLimit: 999,
resize: true,
spinner: '<div class="loading-spinner fade" style="width: 200px; margin-left: -100px;"><div class="progress progress-striped active"><div class="bar" style="width: 100%;"></div></div></div>'
};
$.fn.modalmanager.Constructor = ModalManager
}(jQuery);

View File

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

View File

@ -1,8 +1,99 @@
//~~ View models //~~ View models
function ConnectionViewModel() { function LoginStateViewModel() {
var self = this; var self = this;
self.loggedIn = ko.observable(false);
self.username = ko.observable(undefined);
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() + "\"";
} else {
return "Login";
}
})
self.subscribers = [];
self.subscribe = function(callback) {
self.subscribers.push(callback);
}
self.requestData = function() {
$.ajax({
url: AJAX_BASEURL + "login",
type: "POST",
data: {"passive": true},
success: self.fromResponse
})
}
self.fromResponse = function(response) {
if (response && response.name) {
self.loggedIn(true);
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", {}); });
}
}
self.login = function() {
var username = $("#login_user").val();
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 as \"" + response.name + "\"", type: "success"});
self.fromResponse(response);
},
error: function(jqXHR, textStatus, errorThrown) {
$.pnotify({title: "Login failed", text: "User unknown or wrong password", type: "error"});
}
})
}
self.logout = function() {
$.ajax({
url: AJAX_BASEURL + "logout",
type: "POST",
success: function(response) {
$.pnotify({title: "Logout successful", text: "You are now logged out", type: "success"});
self.fromResponse(response);
}
})
}
}
function ConnectionViewModel(loginStateViewModel) {
var self = this;
self.loginState = loginStateViewModel;
self.portOptions = ko.observableArray(undefined); self.portOptions = ko.observableArray(undefined);
self.baudrateOptions = ko.observableArray(undefined); self.baudrateOptions = ko.observableArray(undefined);
self.selectedPort = ko.observable(undefined); self.selectedPort = ko.observable(undefined);
@ -28,7 +119,7 @@ function ConnectionViewModel() {
self.requestData = function() { self.requestData = function() {
$.ajax({ $.ajax({
url: AJAX_BASEURL + "control/connectionOptions", url: AJAX_BASEURL + "control/connection/options",
method: "GET", method: "GET",
dataType: "json", dataType: "json",
success: function(response) { success: function(response) {
@ -83,6 +174,7 @@ function ConnectionViewModel() {
self.connect = function() { self.connect = function() {
if (self.isErrorOrClosed()) { if (self.isErrorOrClosed()) {
var data = { var data = {
"command": "connect",
"port": self.selectedPort(), "port": self.selectedPort(),
"baudrate": self.selectedBaudrate() "baudrate": self.selectedBaudrate()
}; };
@ -91,7 +183,7 @@ function ConnectionViewModel() {
data["save"] = true; data["save"] = true;
$.ajax({ $.ajax({
url: AJAX_BASEURL + "control/connect", url: AJAX_BASEURL + "control/connection",
type: "POST", type: "POST",
dataType: "json", dataType: "json",
data: data data: data
@ -99,17 +191,20 @@ function ConnectionViewModel() {
} else { } else {
self.requestData(); self.requestData();
$.ajax({ $.ajax({
url: AJAX_BASEURL + "control/disconnect", url: AJAX_BASEURL + "control/connection",
type: "POST", type: "POST",
dataType: "json" dataType: "json",
data: {"command": "disconnect"}
}) })
} }
} }
} }
function PrinterStateViewModel() { function PrinterStateViewModel(loginStateViewModel) {
var self = this; var self = this;
self.loginState = loginStateViewModel;
self.stateString = ko.observable(undefined); self.stateString = ko.observable(undefined);
self.isErrorOrClosed = ko.observable(undefined); self.isErrorOrClosed = ko.observable(undefined);
self.isOperational = ko.observable(undefined); self.isOperational = ko.observable(undefined);
@ -200,11 +295,45 @@ function PrinterStateViewModel() {
self._processZData = function(data) { self._processZData = function(data) {
self.currentHeight(data); self.currentHeight(data);
} }
self.print = function() {
var printAction = function() {
self._jobCommand("start");
}
if (self.isPaused()) {
$("#confirmation_dialog .confirmation_dialog_message").text("This will restart the print job from the beginning.");
$("#confirmation_dialog .confirmation_dialog_acknowledge").click(function(e) {e.preventDefault(); $("#confirmation_dialog").modal("hide"); printAction(); });
$("#confirmation_dialog").modal("show");
} else {
printAction();
}
}
self.pause = function() {
self._jobCommand("pause");
}
self.cancel = function() {
self._jobCommand("cancel");
}
self._jobCommand = function(command) {
$.ajax({
url: AJAX_BASEURL + "control/job",
type: "POST",
dataType: "json",
data: {command: command}
});
}
} }
function TemperatureViewModel(settingsViewModel) { function TemperatureViewModel(loginStateViewModel, settingsViewModel) {
var self = this; var self = this;
self.loginState = loginStateViewModel;
self.temp = ko.observable(undefined); self.temp = ko.observable(undefined);
self.bedTemp = ko.observable(undefined); self.bedTemp = ko.observable(undefined);
self.targetTemp = ko.observable(undefined); self.targetTemp = ko.observable(undefined);
@ -376,9 +505,11 @@ function TemperatureViewModel(settingsViewModel) {
} }
} }
function ControlsViewModel() { function ControlViewModel(loginStateViewModel) {
var self = this; var self = this;
self.loginState = loginStateViewModel;
self.isErrorOrClosed = ko.observable(undefined); self.isErrorOrClosed = ko.observable(undefined);
self.isOperational = ko.observable(undefined); self.isOperational = ko.observable(undefined);
self.isPrinting = ko.observable(undefined); self.isPrinting = ko.observable(undefined);
@ -531,66 +662,10 @@ function ControlsViewModel() {
} }
function SpeedViewModel() { function TerminalViewModel(loginStateViewModel) {
var self = this; var self = this;
self.outerWall = ko.observable(undefined); self.loginState = loginStateViewModel;
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() {
var self = this;
self.log = []; self.log = [];
@ -655,9 +730,11 @@ function TerminalViewModel() {
} }
} }
function GcodeFilesViewModel() { function GcodeFilesViewModel(loginStateViewModel) {
var self = this; var self = this;
self.loginState = loginStateViewModel;
self.isErrorOrClosed = ko.observable(undefined); self.isErrorOrClosed = ko.observable(undefined);
self.isOperational = ko.observable(undefined); self.isOperational = ko.observable(undefined);
self.isPrinting = ko.observable(undefined); self.isPrinting = ko.observable(undefined);
@ -700,11 +777,11 @@ function GcodeFilesViewModel() {
); );
self.isLoadActionPossible = ko.computed(function() { 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() { self.isLoadAndPrintActionPossible = ko.computed(function() {
return self.isOperational() && self.isLoadActionPossible(); return self.loginState.isUser() && self.isOperational() && self.isLoadActionPossible();
}); });
self.fromCurrentData = function(data) { self.fromCurrentData = function(data) {
@ -789,9 +866,11 @@ function GcodeFilesViewModel() {
} }
function WebcamViewModel() { function TimelapseViewModel(loginStateViewModel) {
var self = this; var self = this;
self.loginState = loginStateViewModel;
self.timelapseType = ko.observable(undefined); self.timelapseType = ko.observable(undefined);
self.timelapseTimedInterval = ko.observable(undefined); self.timelapseTimedInterval = ko.observable(undefined);
@ -848,7 +927,6 @@ function WebcamViewModel() {
dataType: "json", dataType: "json",
success: self.fromResponse success: self.fromResponse
}); });
$("#webcam_image").attr("src", CONFIG_WEBCAM_STREAM + "?" + new Date().getTime());
} }
self.fromResponse = function(response) { self.fromResponse = function(response) {
@ -909,9 +987,11 @@ function WebcamViewModel() {
} }
} }
function GcodeViewModel() { function GcodeViewModel(loginStateViewModel) {
var self = this; var self = this;
self.loginState = loginStateViewModel;
self.loadedFilename = undefined; self.loadedFilename = undefined;
self.status = 'idle'; self.status = 'idle';
self.enabled = false; self.enabled = false;
@ -972,9 +1052,202 @@ function GcodeViewModel() {
} }
function SettingsViewModel() { function UsersViewModel(loginStateViewModel) {
var self = this; 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() {
if (!CONFIG_ACCESS_CONTROL) return;
$.ajax({
url: AJAX_BASEURL + "users",
type: "GET",
dataType: "json",
success: self.fromResponse
});
}
self.fromResponse = function(response) {
self.listHelper.updateItems(response.users);
}
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
self.currentUser(undefined);
$("#settings-usersDialogAddUser").modal("hide");
});
}
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();
// make AJAX call
self.updateUser(user, function() {
// close dialog
self.currentUser(undefined);
$("#settings-usersDialogEditUser").modal("hide");
});
}
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);
$("#settings-usersDialogChangePassword").modal("hide");
});
}
//~~ AJAX calls
self.addUser = function(user, callback) {
if (!CONFIG_ACCESS_CONTROL) return;
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 (!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"});
return;
}
$.ajax({
url: AJAX_BASEURL + "users/" + user.name,
type: "DELETE",
success: function(response) {
self.fromResponse(response);
callback();
}
});
}
self.updateUser = function(user, callback) {
if (!CONFIG_ACCESS_CONTROL) return;
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) {
if (!CONFIG_ACCESS_CONTROL) return;
$.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_name = ko.observable(undefined);
self.appearance_color = ko.observable(undefined); self.appearance_color = ko.observable(undefined);
@ -1020,7 +1293,7 @@ function SettingsViewModel() {
type: "GET", type: "GET",
dataType: "json", dataType: "json",
success: self.fromResponse success: self.fromResponse
}) });
} }
self.fromResponse = function(response) { self.fromResponse = function(response) {
@ -1107,11 +1380,13 @@ function SettingsViewModel() {
} }
function NavigationViewModel(appearanceViewModel, settingsViewModel) { function NavigationViewModel(loginStateViewModel, appearanceViewModel, settingsViewModel, usersViewModel) {
var self = this; var self = this;
self.loginState = loginStateViewModel;
self.appearance = appearanceViewModel; self.appearance = appearanceViewModel;
self.systemActions = settingsViewModel.system_actions; self.systemActions = settingsViewModel.system_actions;
self.users = usersViewModel;
self.triggerAction = function(action) { self.triggerAction = function(action) {
var callback = function() { var callback = function() {
@ -1138,24 +1413,26 @@ function NavigationViewModel(appearanceViewModel, settingsViewModel) {
} }
} }
function DataUpdater(connectionViewModel, printerStateViewModel, temperatureViewModel, controlsViewModel, speedViewModel, terminalViewModel, gcodeFilesViewModel, webcamViewModel, gcodeViewModel) { function DataUpdater(loginStateViewModel, connectionViewModel, printerStateViewModel, temperatureViewModel, controlViewModel, terminalViewModel, gcodeFilesViewModel, timelapseViewModel, gcodeViewModel) {
var self = this; var self = this;
self.loginStateViewModel = loginStateViewModel;
self.connectionViewModel = connectionViewModel; self.connectionViewModel = connectionViewModel;
self.printerStateViewModel = printerStateViewModel; self.printerStateViewModel = printerStateViewModel;
self.temperatureViewModel = temperatureViewModel; self.temperatureViewModel = temperatureViewModel;
self.controlsViewModel = controlsViewModel; self.controlViewModel = controlViewModel;
self.terminalViewModel = terminalViewModel; self.terminalViewModel = terminalViewModel;
self.speedViewModel = speedViewModel;
self.gcodeFilesViewModel = gcodeFilesViewModel; self.gcodeFilesViewModel = gcodeFilesViewModel;
self.webcamViewModel = webcamViewModel; self.timelapseViewModel = timelapseViewModel;
self.gcodeViewModel = gcodeViewModel; self.gcodeViewModel = gcodeViewModel;
self._socket = io.connect(); self._socket = io.connect();
self._socket.on("connect", function() { self._socket.on("connect", function() {
if ($("#offline_overlay").is(":visible")) { if ($("#offline_overlay").is(":visible")) {
$("#offline_overlay").hide(); $("#offline_overlay").hide();
self.webcamViewModel.requestData(); self.timelapseViewModel.requestData();
$("#webcam_image").attr("src", CONFIG_WEBCAM_STREAM + "?" + new Date().getTime());
self.loginStateViewModel.requestData();
} }
}) })
self._socket.on("disconnect", function() { self._socket.on("disconnect", function() {
@ -1177,9 +1454,9 @@ function DataUpdater(connectionViewModel, printerStateViewModel, temperatureView
self.connectionViewModel.fromHistoryData(data); self.connectionViewModel.fromHistoryData(data);
self.printerStateViewModel.fromHistoryData(data); self.printerStateViewModel.fromHistoryData(data);
self.temperatureViewModel.fromHistoryData(data); self.temperatureViewModel.fromHistoryData(data);
self.controlsViewModel.fromHistoryData(data); self.controlViewModel.fromHistoryData(data);
self.terminalViewModel.fromHistoryData(data); self.terminalViewModel.fromHistoryData(data);
self.webcamViewModel.fromHistoryData(data); self.timelapseViewModel.fromHistoryData(data);
self.gcodeViewModel.fromHistoryData(data); self.gcodeViewModel.fromHistoryData(data);
self.gcodeFilesViewModel.fromCurrentData(data); self.gcodeFilesViewModel.fromCurrentData(data);
}) })
@ -1187,9 +1464,9 @@ function DataUpdater(connectionViewModel, printerStateViewModel, temperatureView
self.connectionViewModel.fromCurrentData(data); self.connectionViewModel.fromCurrentData(data);
self.printerStateViewModel.fromCurrentData(data); self.printerStateViewModel.fromCurrentData(data);
self.temperatureViewModel.fromCurrentData(data); self.temperatureViewModel.fromCurrentData(data);
self.controlsViewModel.fromCurrentData(data); self.controlViewModel.fromCurrentData(data);
self.terminalViewModel.fromCurrentData(data); self.terminalViewModel.fromCurrentData(data);
self.webcamViewModel.fromCurrentData(data); self.timelapseViewModel.fromCurrentData(data);
self.gcodeViewModel.fromCurrentData(data); self.gcodeViewModel.fromCurrentData(data);
self.gcodeFilesViewModel.fromCurrentData(data); self.gcodeFilesViewModel.fromCurrentData(data);
}) })
@ -1451,32 +1728,34 @@ function AppearanceViewModel(settingsViewModel) {
$(function() { $(function() {
//~~ View models //~~ View models
var connectionViewModel = new ConnectionViewModel(); var loginStateViewModel = new LoginStateViewModel(loginStateViewModel);
var printerStateViewModel = new PrinterStateViewModel(); var usersViewModel = new UsersViewModel(loginStateViewModel);
var settingsViewModel = new SettingsViewModel(); var connectionViewModel = new ConnectionViewModel(loginStateViewModel);
var printerStateViewModel = new PrinterStateViewModel(loginStateViewModel);
var settingsViewModel = new SettingsViewModel(loginStateViewModel, usersViewModel);
var appearanceViewModel = new AppearanceViewModel(settingsViewModel); var appearanceViewModel = new AppearanceViewModel(settingsViewModel);
var temperatureViewModel = new TemperatureViewModel(settingsViewModel); var temperatureViewModel = new TemperatureViewModel(loginStateViewModel, settingsViewModel);
var controlsViewModel = new ControlsViewModel(); var controlViewModel = new ControlViewModel(loginStateViewModel);
var speedViewModel = new SpeedViewModel(); var terminalViewModel = new TerminalViewModel(loginStateViewModel);
var terminalViewModel = new TerminalViewModel(); var gcodeFilesViewModel = new GcodeFilesViewModel(loginStateViewModel);
var gcodeFilesViewModel = new GcodeFilesViewModel(); var timelapseViewModel = new TimelapseViewModel(loginStateViewModel);
var webcamViewModel = new WebcamViewModel(); var gcodeViewModel = new GcodeViewModel(loginStateViewModel);
var gcodeViewModel = new GcodeViewModel(); var navigationViewModel = new NavigationViewModel(loginStateViewModel, appearanceViewModel, settingsViewModel, usersViewModel);
var navigationViewModel = new NavigationViewModel(appearanceViewModel, settingsViewModel);
var dataUpdater = new DataUpdater( var dataUpdater = new DataUpdater(
loginStateViewModel,
connectionViewModel, connectionViewModel,
printerStateViewModel, printerStateViewModel,
temperatureViewModel, temperatureViewModel,
controlsViewModel, controlViewModel,
speedViewModel,
terminalViewModel, terminalViewModel,
gcodeFilesViewModel, gcodeFilesViewModel,
webcamViewModel, timelapseViewModel,
gcodeViewModel gcodeViewModel
); );
//work around a stupid iOS6 bug where ajax requests get cached and only work once, as described at http://stackoverflow.com/questions/12506897/is-safari-on-ios-6-caching-ajax-results // work around a stupid iOS6 bug where ajax requests get cached and only work once, as described at
// http://stackoverflow.com/questions/12506897/is-safari-on-ios-6-caching-ajax-results
$.ajaxSetup({ $.ajaxSetup({
type: 'POST', type: 'POST',
headers: { "cache-control": "no-cache" } headers: { "cache-control": "no-cache" }
@ -1492,31 +1771,7 @@ $(function() {
return false; return false;
}) })
//~~ Print job control //~~ Print job control (should move to PrinterStateViewModel)
$("#job_print").click(function() {
$.ajax({
url: AJAX_BASEURL + "control/print",
type: "POST",
dataType: "json",
success: function(){}
})
})
$("#job_pause").click(function() {
$("#job_pause").button("toggle");
$.ajax({
url: AJAX_BASEURL + "control/pause",
type: "POST",
dataType: "json"
})
})
$("#job_cancel").click(function() {
$.ajax({
url: AJAX_BASEURL + "control/cancel",
type: "POST",
dataType: "json"
})
})
//~~ Temperature control (should really move to knockout click binding) //~~ Temperature control (should really move to knockout click binding)
@ -1542,34 +1797,22 @@ $(function() {
}) })
$('#tabs a[data-toggle="tab"]').on('shown', function (e) { $('#tabs a[data-toggle="tab"]').on('shown', function (e) {
temperatureViewModel.updatePlot(); temperatureViewModel.updatePlot();
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
$("#terminal-send").click(function () { $("#terminal-send").click(function () {
var command = $("#terminal-command").val(); var command = $("#terminal-command").val();
/*
var re = /^([gm][0-9]+)(\s.*)?/;
var commandMatch = command.match(re);
if (commandMatch != null) {
command = commandMatch[1].toUpperCase() + ((commandMatch[2] !== undefined) ? commandMatch[2] : "");
}
*/
if (command) { if (command) {
$.ajax({ $.ajax({
url: AJAX_BASEURL + "control/command", url: AJAX_BASEURL + "control/command",
@ -1613,23 +1856,6 @@ $(function() {
//~~ Offline overlay //~~ Offline overlay
$("#offline_overlay_reconnect").click(function() {dataUpdater.reconnect()}); $("#offline_overlay_reconnect").click(function() {dataUpdater.reconnect()});
//~~ Alert
/*
function displayAlert(text, timeout, type) {
var placeholder = $("#alert_placeholder");
var alertType = "";
if (type == "success" || type == "error" || type == "info") {
alertType = " alert-" + type;
}
placeholder.append($("<div id='activeAlert' class='alert " + alertType + " fade in' data-alert='alert'><p>" + text + "</p></div>"));
placeholder.fadeIn();
$("#activeAlert").delay(timeout).fadeOut("slow", function() {$(this).remove(); $("#alert_placeholder").hide();});
}
*/
//~~ knockout.js bindings //~~ knockout.js bindings
ko.bindingHandlers.popover = { ko.bindingHandlers.popover = {
@ -1649,34 +1875,46 @@ $(function() {
} }
} }
ko.applyBindings(connectionViewModel, document.getElementById("connection")); ko.applyBindings(connectionViewModel, document.getElementById("connection_accordion"));
ko.applyBindings(printerStateViewModel, document.getElementById("state")); ko.applyBindings(printerStateViewModel, document.getElementById("state_accordion"));
ko.applyBindings(gcodeFilesViewModel, document.getElementById("files")); ko.applyBindings(gcodeFilesViewModel, document.getElementById("files_accordion"));
ko.applyBindings(gcodeFilesViewModel, document.getElementById("files-heading"));
ko.applyBindings(temperatureViewModel, document.getElementById("temp")); ko.applyBindings(temperatureViewModel, document.getElementById("temp"));
ko.applyBindings(controlsViewModel, document.getElementById("controls")); ko.applyBindings(controlViewModel, document.getElementById("control"));
ko.applyBindings(terminalViewModel, document.getElementById("term")); ko.applyBindings(terminalViewModel, document.getElementById("term"));
ko.applyBindings(speedViewModel, document.getElementById("speed"));
ko.applyBindings(gcodeViewModel, document.getElementById("gcode")); ko.applyBindings(gcodeViewModel, document.getElementById("gcode"));
ko.applyBindings(settingsViewModel, document.getElementById("settings_dialog")); ko.applyBindings(settingsViewModel, document.getElementById("settings_dialog"));
ko.applyBindings(navigationViewModel, document.getElementById("navbar")); ko.applyBindings(navigationViewModel, document.getElementById("navbar"));
ko.applyBindings(appearanceViewModel, document.getElementsByTagName("head")[0]); ko.applyBindings(appearanceViewModel, document.getElementsByTagName("head")[0]);
var webcamElement = document.getElementById("webcam"); var timelapseElement = document.getElementById("timelapse");
if (webcamElement) { if (timelapseElement) {
ko.applyBindings(webcamViewModel, document.getElementById("webcam")); ko.applyBindings(timelapseViewModel, timelapseElement);
} }
var gCodeVisualizerElement = document.getElementById("gcode"); var gCodeVisualizerElement = document.getElementById("gcode");
if(gCodeVisualizerElement){ if (gCodeVisualizerElement) {
gcodeViewModel.initialize(); gcodeViewModel.initialize();
} }
//~~ startup commands //~~ startup commands
loginStateViewModel.requestData();
connectionViewModel.requestData(); connectionViewModel.requestData();
controlsViewModel.requestData(); controlViewModel.requestData();
gcodeFilesViewModel.requestData(); gcodeFilesViewModel.requestData();
webcamViewModel.requestData(); timelapseViewModel.requestData();
loginStateViewModel.subscribe(function(change, data) {
if ("login" == change) {
$("#gcode_upload").fileupload("enable");
settingsViewModel.requestData(); settingsViewModel.requestData();
if (data.admin) {
usersViewModel.requestData();
}
} else {
$("#gcode_upload").fileupload("disable");
}
})
//~~ UI stuff //~~ UI stuff
@ -1692,6 +1930,16 @@ $(function() {
$.pnotify.defaults.history = false; $.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();
});
} }
); );

View File

@ -8,6 +8,7 @@
<link rel="apple-touch-icon" sizes="144x144" href="{{ url_for('static', filename='img/apple-touch-icon-144x144.png') }}"> <link rel="apple-touch-icon" sizes="144x144" href="{{ url_for('static', filename='img/apple-touch-icon-144x144.png') }}">
<link href="{{ url_for('static', filename='css/bootstrap.min.css') }}" rel="stylesheet" media="screen"> <link href="{{ url_for('static', filename='css/bootstrap.min.css') }}" rel="stylesheet" media="screen">
<link href="{{ url_for('static', filename='css/bootstrap-modal.css') }}" rel="stylesheet" media="screen">
<link href="{{ url_for('static', filename='css/font-awesome.min.css') }}" rel="stylesheet" media="screen"> <link href="{{ url_for('static', filename='css/font-awesome.min.css') }}" rel="stylesheet" media="screen">
<link href="{{ url_for('static', filename='css/jquery.fileupload-ui.css') }}" rel="stylesheet" media="screen"> <link href="{{ url_for('static', filename='css/jquery.fileupload-ui.css') }}" rel="stylesheet" media="screen">
<link href="{{ url_for('static', filename='css/jquery.pnotify.default.css') }}" rel="stylesheet" media="screen"> <link href="{{ url_for('static', filename='css/jquery.pnotify.default.css') }}" rel="stylesheet" media="screen">
@ -16,12 +17,15 @@
<link href="{{ url_for('static', filename='gcodeviewer/css/style.css') }}" rel="stylesheet" media="screen"> <link href="{{ url_for('static', filename='gcodeviewer/css/style.css') }}" rel="stylesheet" media="screen">
<script lang="javascript"> <script lang="javascript">
var AJAX_BASEURL = "/ajax/"; var AJAX_BASEURL = "{{ ajaxBaseUrl }}";
var CONFIG_GCODEFILESPERPAGE = 5; var CONFIG_GCODEFILESPERPAGE = 5;
var CONFIG_TIMELAPSEFILESPERPAGE = 10; var CONFIG_TIMELAPSEFILESPERPAGE = 10;
var CONFIG_USERSPERPAGE = 10;
var CONFIG_WEBCAM_STREAM = "{{ webcamStream }}"; 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; var WEB_SOCKET_DEBUG = true;
</script> </script>
<script src="{{ url_for('static', filename='js/less-1.3.3.min.js') }}" type="text/javascript"></script> <script src="{{ url_for('static', filename='js/less-1.3.3.min.js') }}" type="text/javascript"></script>
@ -33,9 +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> <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"> <div class="nav-collapse">
<ul class="nav pull-right"> <ul class="nav pull-right">
<li><a id="navbar_show_settings" class="pull-right" href="#settings_dialog"><i class="icon-wrench"></i> Settings</a></li> <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 %} {% if enableSystemMenu %}
<li class="dropdown"> <li class="dropdown" style="display: none" data-bind="visible: loginState.isAdmin">
<a href="#" class="dropdown-toggle" data-toggle="dropdown"> <a href="#" class="dropdown-toggle" data-toggle="dropdown">
<i class="icon-off"></i> System <i class="icon-off"></i> System
<b class="caret"></b> <b class="caret"></b>
@ -45,6 +53,28 @@
</ul> </ul>
</li> </li>
{% endif %} {% endif %}
{% if enableAccessControl %}
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<i class="icon-user"></i> <span data-bind="text: loginState.userMenuText">Login</span>
<b class="caret"></b>
</a>
<div id="login_dropdown_loggedout" style="padding: 15px" class="dropdown-menu" data-bind="css: {hide: loginState.loggedIn(), 'dropdown-menu': !loginState.loggedIn()}">
<label for="login_user">Username</label>
<input type="text" id="login_user" placeholder="Username">
<label for="login_pass">Password</label>
<input type="password" id="login_pass" placeholder="Password">
<label class="checkbox">
<input type="checkbox" id="login_remember"> Remember me
</label>
<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>
{% endif %}
</ul> </ul>
</div> </div>
</div> </div>
@ -53,24 +83,24 @@
<div class="container octoprint-container"> <div class="container octoprint-container">
<div class="row"> <div class="row">
<div class="accordion span4"> <div class="accordion span4">
<div class="accordion-group"> <div class="accordion-group" data-bind="visible: loginState.isUser" id="connection_accordion">
<div class="accordion-heading"> <div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" href="#connection"><i class="icon-signal"></i> Connection</a> <a class="accordion-toggle" data-toggle="collapse" href="#connection"><i class="icon-signal"></i> Connection</a>
</div> </div>
<div class="accordion-body collapse in" id="connection"> <div class="accordion-body collapse in" id="connection">
<div class="accordion-inner"> <div class="accordion-inner">
<label for="connection_ports" data-bind="css: {disabled: !isErrorOrClosed}, enable: isErrorOrClosed">Serial Port</label> <label for="connection_ports" data-bind="css: {disabled: !isErrorOrClosed}, enable: isErrorOrClosed && loginState.isUser">Serial Port</label>
<select id="connection_ports" data-bind="options: portOptions, optionsCaption: 'AUTO', value: selectedPort, css: {disabled: !isErrorOrClosed}, enable: isErrorOrClosed"></select> <select id="connection_ports" data-bind="options: portOptions, optionsCaption: 'AUTO', value: selectedPort, css: {disabled: !isErrorOrClosed}, enable: isErrorOrClosed && loginState.isUser"></select>
<label for="connection_baudrates" data-bind="css: {disabled: !isErrorOrClosed}, enable: isErrorOrClosed">Baudrate</label> <label for="connection_baudrates" data-bind="css: {disabled: !isErrorOrClosed}, enable: isErrorOrClosed && loginState.isUser">Baudrate</label>
<select id="connection_baudrates" data-bind="options: baudrateOptions, optionsCaption: 'AUTO', value: selectedBaudrate, css: {disabled: !isErrorOrClosed}, enable: isErrorOrClosed"></select> <select id="connection_baudrates" data-bind="options: baudrateOptions, optionsCaption: 'AUTO', value: selectedBaudrate, css: {disabled: !isErrorOrClosed}, enable: isErrorOrClosed && loginState.isUser"></select>
<label class="checkbox"> <label class="checkbox">
<input type="checkbox" id="connection_save" data-bind="checked: saveSettings, css: {disabled: !isErrorOrClosed}, enable: isErrorOrClosed"> Save connection settings <input type="checkbox" id="connection_save" data-bind="checked: saveSettings, css: {disabled: !isErrorOrClosed}, enable: isErrorOrClosed && loginState.isUser"> Save connection settings
</label> </label>
<button class="btn btn-block" id="printer_connect" data-bind="click: connect, text: buttonText()">Connect</button> <button class="btn btn-block" id="printer_connect" data-bind="click: connect, text: buttonText(), enable: loginState.isUser">Connect</button>
</div> </div>
</div> </div>
</div> </div>
<div class="accordion-group"> <div class="accordion-group" id="state_accordion">
<div class="accordion-heading"> <div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" href="#state"><i class="icon-info-sign"></i> State</a> <a class="accordion-toggle" data-toggle="collapse" href="#state"><i class="icon-info-sign"></i> State</a>
</div> </div>
@ -89,21 +119,21 @@
<div class="bar" id="job_progressBar" data-bind="style: { width: progress() + '%' }"></div> <div class="bar" id="job_progressBar" data-bind="style: { width: progress() + '%' }"></div>
</div> </div>
<div class="row-fluid print-control"> <div class="row-fluid print-control" style="display: none;" data-bind="visible: loginState.isUser">
<button class="btn btn-primary span4" data-bind="enable: isOperational() && isReady() && !isPrinting(), css: {'btn-danger': isPaused()}" id="job_print"><i class="icon-print icon-white"></i> <span data-bind="text: (isPaused() ? 'Restart' : 'Print')">Print</span></button> <button class="btn btn-primary span4" data-bind="click: print, enable: isOperational() && isReady() && !isPrinting() && loginState.isUser(), css: {'btn-danger': isPaused()}" id="job_print"><i class="icon-white" data-bind="css: {'icon-print': !isPaused(), 'icon-undo': isPaused()}"></i> <span data-bind="text: (isPaused() ? 'Restart' : 'Print')">Print</span></button>
<button class="btn span4" id="job_pause" data-bind="css: {active: isPaused}"><i class="icon-pause"></i> <span>Pause</span></button> <button class="btn span4" id="job_pause" data-bind="click: pause, enable: isOperational() && (isPrinting() || isPaused()) && loginState.isUser(), css: {active: isPaused()}"><i class="icon-pause"></i> <span>Pause</span></button>
<button class="btn span4" id="job_cancel"><i class="icon-stop"></i> Cancel</button> <button class="btn span4" id="job_cancel" data-bind="click: cancel, enable: isOperational() && (isPrinting() || isPaused()) && loginState.isUser()"><i class="icon-stop"></i> Cancel</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="accordion-group"> <div class="accordion-group" id="files_accordion">
<div class="accordion-heading" id="files-heading"> <div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" href="#files"><i class="icon-list"></i> Files</a> <a class="accordion-toggle" data-toggle="collapse" href="#files"><i class="icon-list"></i> Files</a>
<div class="settings-trigger btn-group"> <div class="settings-trigger btn-group">
<a class="dropdown-toggle" data-toggle="dropdown" href="#"> <a class="dropdown-toggle" data-toggle="dropdown" href="#">
<i class="icon-wrench"></i> <span class="icon-wrench"></span>
</a> </a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a href="#" data-bind="click: function() { $root.listHelper.changeSorting('name'); }"><i class="icon-ok" data-bind="style: {visibility: listHelper.currentSorting() == 'name' ? 'visible' : 'hidden'}"></i> Sort by name (ascending)</a></li> <li><a href="#" data-bind="click: function() { $root.listHelper.changeSorting('name'); }"><i class="icon-ok" data-bind="style: {visibility: listHelper.currentSorting() == 'name' ? 'visible' : 'hidden'}"></i> Sort by name (ascending)</a></li>
@ -129,7 +159,7 @@
<td class="gcode_files_name" data-bind="text: name"></td> <td class="gcode_files_name" data-bind="text: name"></td>
<td class="gcode_files_size" data-bind="text: size"></td> <td class="gcode_files_size" data-bind="text: size"></td>
<td class="gcode_files_action"> <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> </td>
</tr> </tr>
</tbody> </tbody>
@ -145,10 +175,11 @@
<li data-bind="css: {disabled: listHelper.currentPage() === listHelper.lastPage()}"><a href="#" data-bind="click: listHelper.nextPage">»</a></li> <li data-bind="css: {disabled: listHelper.currentPage() === listHelper.lastPage()}"><a href="#" data-bind="click: listHelper.nextPage">»</a></li>
</ul> </ul>
</div> </div>
<span class="btn btn-primary btn-block fileinput-button" style="margin-bottom: 10px"> <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> <i class="icon-upload icon-white"></i>
<span>Upload</span> <span>Upload</span>
<input id="gcode_upload" type="file" name="gcode_file" class="fileinput-button" data-url="/ajax/gcodefiles/upload"> <input id="gcode_upload" type="file" name="gcode_file" class="fileinput-button" data-url="/ajax/gcodefiles/upload" data-bind="enable: loginState.isUser()">
</span> </span>
<div id="gcode_upload_progress" class="progress" style="width: 100%;"> <div id="gcode_upload_progress" class="progress" style="width: 100%;">
<div class="bar" style="width: 0%"></div> <div class="bar" style="width: 0%"></div>
@ -160,15 +191,15 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="tabbable span8"> <div class="span8 tabbable">
<ul class="nav nav-tabs" id="tabs"> <ul class="nav nav-tabs" id="tabs">
<li class="active"><a href="#temp" data-toggle="tab">Temperature</a></li> <li class="active"><a href="#temp" data-toggle="tab">Temperature</a></li>
<li><a href="#controls" data-toggle="tab">Controls</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 %} {% 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> <li><a href="#term" data-toggle="tab">Terminal</a></li>
{% if webcamStream %}<li><a href="#webcam" data-toggle="tab">Webcam</a></li>{% endif %} {% if enableTimelapse %}<li><a href="#timelapse" data-toggle="tab">Timelapse</a></li>{% endif %}
</ul> </ul>
<div class="tab-content"> <div class="tab-content">
@ -184,14 +215,15 @@
<label>Target: <strong data-bind="html: targetTempString"></strong></label> <label>Target: <strong data-bind="html: targetTempString"></strong></label>
<div style="display: none;" data-bind="visible: loginState.isUser">
<label for="temp_newTemp">New Target</label> <label for="temp_newTemp">New Target</label>
<div class="input-append"> <div class="input-append">
<input type="text" id="temp_newTemp" data-bind="attr: {placeholder: targetTemp}" class="tempInput"> <input type="text" id="temp_newTemp" data-bind="attr: {placeholder: targetTemp}, enable: isOperational() && loginState.isUser()" class="tempInput">
<span class="add-on">&deg;C</span> <span class="add-on">&deg;C</span>
</div> </div>
<div class="btn-group"> <div class="btn-group">
<button type="submit" class="btn" id="temp_newTemp_set">Set</button> <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"> <button class="btn dropdown-toggle" data-toggle="dropdown" data-bind="enable: isOperational() && loginState.isUser()">
<span class="caret"></span> <span class="caret"></span>
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
@ -207,6 +239,7 @@
</ul> </ul>
</div> </div>
</div> </div>
</div>
<div class="form-horizontal span6"> <div class="form-horizontal span6">
<h1>Bed Temperature</h1> <h1>Bed Temperature</h1>
@ -214,14 +247,15 @@
<label>Target: <strong data-bind="html: bedTargetTempString"></strong></label> <label>Target: <strong data-bind="html: bedTargetTempString"></strong></label>
<div style="display: none;" data-bind="visible: loginState.isUser">
<label for="temp_newBedTemp">New Target</label> <label for="temp_newBedTemp">New Target</label>
<div class="input-append"> <div class="input-append">
<input type="text" id="temp_newBedTemp" data-bind="attr: {placeholder: bedTargetTemp}" class="tempInput"> <input type="text" id="temp_newBedTemp" data-bind="attr: {placeholder: bedTargetTemp}, enable: isOperational() && loginState.isUser()" class="tempInput">
<span class="add-on">&deg;C</span> <span class="add-on">&deg;C</span>
</div> </div>
<div class="btn-group"> <div class="btn-group">
<button type="submit" class="btn" id="temp_newBedTemp_set">Set</button> <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"> <button class="btn dropdown-toggle" data-toggle="dropdown" data-bind="enable: isOperational() && loginState.isUser()">
<span class="caret"></span> <span class="caret"></span>
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
@ -239,70 +273,77 @@
</div> </div>
</div> </div>
</div> </div>
<div class="tab-pane" id="controls"> </div>
<div class="jog-panel"> <div class="tab-pane" id="control">
{% if webcamStream %}
<div id="webcam_container">
<img id="webcam_image" src="{{ webcamStream }}">
</div>
{% endif %}
<div class="jog-panel" style="display: none;" data-bind="visible: loginState.isUser">
<!-- XY jogging control panel --> <!-- XY jogging control panel -->
<div class="jog-panel"> <div class="jog-panel">
<h1>X/Y</h1> <h1>X/Y</h1>
<div> <div>
<button class="btn box" data-bind="enable: isOperational() && !isPrinting(), click: function() { $root.sendJogCommand('y',1) }"><i class="icon-arrow-up"></i></button> <button class="btn box" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendJogCommand('y',1) }"><i class="icon-arrow-up"></i></button>
</div> </div>
<div> <div>
<button class="btn box pull-left" data-bind="enable: isOperational() && !isPrinting(), click: function() { $root.sendJogCommand('x',-1) }"><i class="icon-arrow-left"></i></button> <button class="btn box pull-left" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendJogCommand('x',-1) }"><i class="icon-arrow-left"></i></button>
<button class="btn box pull-left" data-bind="enable: isOperational() && !isPrinting(), click: function() { $root.sendHomeCommand('XY') }"><i class="icon-home"></i></button> <button class="btn box pull-left" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendHomeCommand('XY') }"><i class="icon-home"></i></button>
<button class="btn box pull-left" data-bind="enable: isOperational() && !isPrinting(), click: function() { $root.sendJogCommand('x',1) }"><i class="icon-arrow-right"></i></button> <button class="btn box pull-left" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendJogCommand('x',1) }"><i class="icon-arrow-right"></i></button>
</div> </div>
<div> <div>
<button class="btn box" data-bind="enable: isOperational() && !isPrinting(), click: function() { $root.sendJogCommand('y',-1) }"><i class="icon-arrow-down"></i></button> <button class="btn box" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendJogCommand('y',-1) }"><i class="icon-arrow-down"></i></button>
</div> </div>
</div> </div>
<!-- Z jogging control panel --> <!-- Z jogging control panel -->
<div class="jog-panel"> <div class="jog-panel">
<h1>Z</h1> <h1>Z</h1>
<div> <div>
<button class="btn box" data-bind="enable: isOperational() && !isPrinting(), click: function() { $root.sendJogCommand('z',1) }"><i class="icon-arrow-up"></i></button> <button class="btn box" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendJogCommand('z',1) }"><i class="icon-arrow-up"></i></button>
</div> </div>
<div> <div>
<button class="btn box" data-bind="enable: isOperational() && !isPrinting(), click: function() { $root.sendHomeCommand('Z') }"><i class="icon-home"></i></button> <button class="btn box" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendHomeCommand('Z') }"><i class="icon-home"></i></button>
</div> </div>
<div> <div>
<button class="btn box" data-bind="enable: isOperational() && !isPrinting(), click: function() { $root.sendJogCommand('z',-1) }"><i class="icon-arrow-down"></i></button> <button class="btn box" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendJogCommand('z',-1) }"><i class="icon-arrow-down"></i></button>
</div> </div>
</div> </div>
<!-- Jog distance --> <!-- Jog distance -->
<div class="distance"> <div class="distance">
<div class="btn-group" data-toggle="buttons-radio" id="jog_distance"> <div class="btn-group" data-toggle="buttons-radio" id="jog_distance">
<button type="button" class="btn" data-distance="0.1">0.1</button> <button type="button" class="btn" data-distance="0.1" data-bind="enable: loginState.isUser()">0.1</button>
<button type="button" class="btn" data-distance="1">1</button> <button type="button" class="btn" data-distance="1" data-bind="enable: loginState.isUser()">1</button>
<button type="button" class="btn active" data-distance="10">10</button> <button type="button" class="btn active" data-distance="10" data-bind="enable: loginState.isUser()">10</button>
<button type="button" class="btn" data-distance="100">100</button> <button type="button" class="btn" data-distance="100" data-bind="enable: loginState.isUser()">100</button>
</div> </div>
</div> </div>
</div> </div>
<!-- Extrusion control panel --> <!-- Extrusion control panel -->
<div class="jog-panel"> <div class="jog-panel" style="display: none;" data-bind="visible: loginState.isUser">
<h1>E</h1> <h1>E</h1>
<div> <div>
<div class="input-append control-box"> <div class="input-append control-box">
<input type="text" class="input-mini text-right" data-bind="value: extrusionAmount, enable: isOperational() && !isPrinting(), attr: {placeholder: 5}"> <input type="text" class="input-mini text-right" data-bind="value: extrusionAmount, enable: isOperational() && !isPrinting() && loginState.isUser(), attr: {placeholder: 5}">
<span class="add-on">mm</span> <span class="add-on">mm</span>
</div> </div>
<button class="btn control-box" data-bind="enable: isOperational() && !isPrinting(), click: function() { $root.sendExtrudeCommand() }">Extrude</button> <button class="btn control-box" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendExtrudeCommand() }">Extrude</button>
<button class="btn control-box" data-bind="enable: isOperational() && !isPrinting(), click: function() { $root.sendRetractCommand() }">Retract</button> <button class="btn control-box" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendRetractCommand() }">Retract</button>
</div> </div>
</div> </div>
<!-- General control panel --> <!-- General control panel -->
<div class="jog-panel"> <div class="jog-panel" style="display: none;" data-bind="visible: loginState.isUser">
<h1>General</h1> <h1>General</h1>
<div> <div>
<button class="btn control-box" data-bind="enable: isOperational() && !isPrinting(), click: function() { $root.sendCustomCommand({type:'command',command:'M18'}) }"><i class="icon-off"></i>&nbsp;Motors off</button> <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>
<button class="btn control-box" data-bind="enable: isOperational(), click: function() { $root.sendCustomCommand({type:'command',command:'M106'}) }">Fans on</button> <button class="btn control-box" data-bind="enable: isOperational() && loginState.isUser(), click: function() { $root.sendCustomCommand({type:'command',command:'M106'}) }">Fans on</button>
<button class="btn control-box" data-bind="enable: isOperational(), click: function() { $root.sendCustomCommand({type:'command',command:'M106 S0'}) }">Fans off</button> <button class="btn control-box" data-bind="enable: isOperational() && loginState.isUser(), click: function() { $root.sendCustomCommand({type:'command',command:'M106 S0'}) }">Fans off</button>
</div> </div>
</div> </div>
<!-- Container for custom controls --> <!-- 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 --> <!-- Templates for custom controls -->
<script type="text/html" id="customControls_sectionTemplate"> <script type="text/html" id="customControls_sectionTemplate">
@ -312,7 +353,7 @@
</script> </script>
<script type="text/html" id="customControls_commandTemplate"> <script type="text/html" id="customControls_commandTemplate">
<form class="form-inline"> <form class="form-inline">
<button class="btn" data-bind="text: name, enable: $root.isOperational(), click: function() { $root.sendCustomCommand($data) }"></button> <button class="btn" data-bind="text: name, enable: $root.isOperational() && $root.loginState.isUser(), click: function() { $root.sendCustomCommand($data) }"></button>
</form> </form>
</script> </script>
<script type="text/html" id="customControls_parametricCommandTemplate"> <script type="text/html" id="customControls_parametricCommandTemplate">
@ -321,43 +362,12 @@
<label data-bind="text: name"></label> <label data-bind="text: name"></label>
<input type="text" class="input-small" data-bind="attr: {placeholder: name}, value: value"> <input type="text" class="input-small" data-bind="attr: {placeholder: name}, value: value">
<!-- /ko --> <!-- /ko -->
<button class="btn" data-bind="text: name, enable: $root.isOperational(), click: function() { $root.sendCustomCommand($data) }"></button> <button class="btn" data-bind="text: name, enable: $root.isOperational() && $root.loginState.isUser(), click: function() { $root.sendCustomCommand($data) }"></button>
</form> </form>
</script> </script>
<script type="text/html" id="customControls_emptyTemplate"><div></div></script> <script type="text/html" id="customControls_emptyTemplate"><div></div></script>
<!-- End of templates for custom controls --> <!-- End of templates for custom controls -->
</div> </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(), attr: {placeholder: outerWall}">
<span class="add-on">%</span>
<button type="submit" class="btn" id="speed_outerWall_set" data-bind="enable: isOperational()">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(), attr: {placeholder: innerWall}">
<span class="add-on">%</span>
<button type="submit" class="btn" id="speed_innerWall_set" data-bind="enable: isOperational()">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(), attr: {placeholder: fill}">
<span class="add-on">%</span>
<button type="submit" class="btn" id="speed_fill_set" data-bind="enable: isOperational()">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(), attr: {placeholder: support}">
<span class="add-on">%</span>
<button type="submit" class="btn" id="speed_support_set" data-bind="enable: isOperational()">Set</button>
</div>
</div>
</div>
<div class="tab-pane" id="gcode"> <div class="tab-pane" id="gcode">
<canvas id="canvas" width="572" height="588"></canvas> <canvas id="canvas" width="572" height="588"></canvas>
<div id="slider-vertical"></div> <div id="slider-vertical"></div>
@ -451,22 +461,18 @@
<input type="checkbox" id="terminal-autoscroll" data-bind="checked: autoscrollEnabled"> Autoscroll <input type="checkbox" id="terminal-autoscroll" data-bind="checked: autoscrollEnabled"> Autoscroll
</label> </label>
<div class="input-append"> <div class="input-append" style="display: none;" data-bind="visible: loginState.isUser">
<input type="text" id="terminal-command"> <input type="text" id="terminal-command" data-bind="enable: isOperational() && loginState.isUser()">
<button class="btn" type="button" id="terminal-send">Send</button> <button class="btn" type="button" id="terminal-send" data-bind="enable: isOperational() && loginState.isUser()">Send</button>
</div> </div>
</div> </div>
{% if webcamStream %}
<div class="tab-pane" id="webcam">
<div id="webcam_container">
<img id="webcam_image" src="{{ webcamStream }}">
</div>
{% if enableTimelapse %} {% if enableTimelapse %}
<div class="tab-pane" id="timelapse">
<div style="display: none;" data-bind="visible: loginState.isUser">
<h1>Timelapse Configuration</h1> <h1>Timelapse Configuration</h1>
<label for="webcam_timelapse_mode">Timelapse Mode</label> <label for="webcam_timelapse_mode">Timelapse Mode</label>
<select id="webcam_timelapse_mode" data-bind="value: timelapseType, enable: isOperational() && !isPrinting()"> <select id="webcam_timelapse_mode" data-bind="value: timelapseType, enable: isOperational() && !isPrinting() && loginState.isUser()">
<option value="off">Off</option> <option value="off">Off</option>
<option value="zchange">On Z Change</option> <option value="zchange">On Z Change</option>
<option value="timed">Timed</option> <option value="timed">Timed</option>
@ -475,13 +481,14 @@
<div id="webcam_timelapse_timedsettings" data-bind="visible: intervalInputEnabled()"> <div id="webcam_timelapse_timedsettings" data-bind="visible: intervalInputEnabled()">
<label for="webcam_timelapse_interval">Interval</label> <label for="webcam_timelapse_interval">Interval</label>
<div class="input-append"> <div class="input-append">
<input type="text" class="input-mini" id="webcam_timelapse_interval" data-bind="value: timelapseTimedInterval, enable: isOperational() && !isPrinting()"> <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> <span class="add-on">sec</span>
</div> </div>
</div> </div>
<div> <div>
<button class="btn" data-bind="click: save, enable: isOperational() && !isPrinting()">Save Settings</button> <button class="btn" data-bind="click: save, enable: isOperational() && !isPrinting() && loginState.isUser()">Save Settings</button>
</div>
</div> </div>
<h1>Finished Timelapses</h1> <h1>Finished Timelapses</h1>
@ -501,7 +508,7 @@
<tr data-bind="attr: {title: name}"> <tr data-bind="attr: {title: name}">
<td class="timelapse_files_name" data-bind="text: name"></td> <td class="timelapse_files_name" data-bind="text: name"></td>
<td class="timelapse_files_size" data-bind="text: size"></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> </tr>
</tbody> </tbody>
</table> </table>
@ -516,7 +523,6 @@
<li data-bind="css: {disabled: listHelper.currentPage() === listHelper.lastPage()}"><a href="#" data-bind="click: listHelper.nextPage">»</a></li> <li data-bind="css: {disabled: listHelper.currentPage() === listHelper.lastPage()}"><a href="#" data-bind="click: listHelper.nextPage">»</a></li>
</ul> </ul>
</div> </div>
{% endif %}
</div> </div>
{% endif %} {% endif %}
</div> </div>
@ -532,24 +538,27 @@
</div> </div>
</div> </div>
{% include 'settings.html' %} {% include 'settings.jinja2' %}
{% include 'dialogs.html' %} {% include 'dialogs.jinja2' %}
<script type="text/javascript" src="http://code.jquery.com/jquery-latest.js"></script> <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/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/underscore.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/knockout.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/bootstrap.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/bootstrap/bootstrap-modalmanager.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/bootstrap/bootstrap-modal.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/jquery.ui.core.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/jquery.ui.widget.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/jquery.ui.mouse.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/jquery.ui.slider.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/jquery.pnotify.min.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/jquery/jquery.flot.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/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='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/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/gCodeReader.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='gcodeviewer/js/renderer.js') }}"></script> <script type="text/javascript" src="{{ url_for('static', filename='gcodeviewer/js/renderer.js') }}"></script>

View File

@ -1,20 +1,21 @@
<div id="settings_dialog" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="settings_dialog_label" aria-hidden="true"> <div id="settings_dialog" class="modal hide fade container" tabindex="-1" role="dialog" aria-labelledby="settings_dialog_label" aria-hidden="true">
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button> <button type="button" class="close" data-dismiss="modal">&times;</button>
<h3 id="settings_dialog_label">OctoPrint Settings</h3> <h3 id="settings_dialog_label">OctoPrint Settings</h3>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="tabbable"> <div class="tabbable">
<ul class="nav nav-pills" id="settingsTabs"> <ul class="nav nav-list span4" id="settingsTabs">
<li class="active"><a href="#settings_printerParameters" data-toggle="tab">Printer Parameters</a></li> <li class="active"><a href="#settings_printerParameters" data-toggle="tab">Printer Parameters</a></li>
<li><a href="#settings_webcam" data-toggle="tab">Webcam</a></li> <li><a href="#settings_webcam" data-toggle="tab">Webcam</a></li>
<li><a href="#settings_features" data-toggle="tab">Features</a></li> <li><a href="#settings_features" data-toggle="tab">Features</a></li>
<li><a href="#settings_folder" data-toggle="tab">Folder</a></li> <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_temperature" data-toggle="tab">Temperature</a></li>
<li><a href="#settings_appearance" data-toggle="tab">Appearance</a></li> <li><a href="#settings_appearance" data-toggle="tab">Appearance</a></li>
{% if enableAccessControl %}<li><a href="#settings_users" data-toggle="tab">Users</a></li>{% endif %}
</ul> </ul>
<div class="tab-content"> <div class="tab-content span8">
<div class="tab-pane active" id="settings_printerParameters"> <div class="tab-pane active" id="settings_printerParameters">
<form class="form-horizontal"> <form class="form-horizontal">
<div class="control-group"> <div class="control-group">
@ -198,6 +199,152 @@
</div> </div>
</form> </form>
</div> </div>
{% if enableAccessControl %}
<div class="tab-pane" id="settings_users">
<table class="table table-condensed table-hover" id="system_users">
<thead>
<tr>
<th class="settings_users_name">Name</th>
<th class="settings_users_active">Active</th>
<th class="settings_users_admin">Admin</th>
<th class="settings_users_actions">Action</th>
</tr>
</thead>
<tbody data-bind="foreach: users.listHelper.paginatedItems">
<tr>
<td class="settings_users_name" data-bind="text: name"></td>
<td class="settings_users_active"><i data-bind="css: { 'icon-check': active, 'icon-check-empty': !active }"></i></td>
<td class="settings_users_admin"><i data-bind="css: { 'icon-check': admin, 'icon-check-empty': !admin }"></i></td>
<td class="settings_users_actions" class="system_users_action">
<a href="#" class="icon-pencil" title="Update User" data-bind="click: function() { $root.users.showEditUserDialog($data); }"></a>&nbsp;|&nbsp;<a href="#" class="icon-key" title="Change password" data-bind="click: function() { $root.users.showChangePasswordDialog($data); }"></a>&nbsp;|&nbsp;<a href="#" class="icon-trash" title="Delete user" data-bind="click: function() { $root.users.removeUser($data); }"></a>
</td>
</tr>
</tbody>
</table>
<div class="pagination pagination-mini pagination-centered">
<ul>
<li data-bind="css: {disabled: users.listHelper.currentPage() === 0}"><a href="#" data-bind="click: users.listHelper.prevPage">«</a></li>
</ul>
<ul data-bind="foreach: users.listHelper.pages">
<li data-bind="css: { active: $data.number === $root.users.listHelper.currentPage(), disabled: $data.number === -1 }"><a href="#" data-bind="text: $data.text, click: function() { $root.users.listHelper.changePage($data.number); }"></a></li>
</ul>
<ul>
<li data-bind="css: {disabled: users.listHelper.currentPage() === users.listHelper.lastPage()}"><a href="#" data-bind="click: users.listHelper.nextPage">»</a></li>
</ul>
</div>
<button title="Add user" class="btn" data-bind="click: $root.users.showAddUserDialog"><i class="icon-plus"></i> Create new user</button>
<!-- Modals for user management -->
<div id="settings-usersDialogAddUser" class="modal hide fade">
<div class="modal-header">
<a href="#" class="close" data-dismiss="modal" aria-hidden="true">&times;</a>
<h3>Create new user</h3>
</div>
<div class="modal-body">
<form class="form-horizontal">
<div class="control-group">
<label class="control-label" for="settings-usersDialogAddUserName">Username</label>
<div class="controls">
<input type="text" class="input-block-level" id="settings-usersDialogAddUserName" data-bind="value: $root.users.editorUsername" required>
</div>
</div>
<div class="control-group">
<label class="control-label" for="settings-usersDialogAddUserPassword1">Password</label>
<div class="controls">
<input type="password" class="input-block-level" id="settings-usersDialogAddUserPassword1" data-bind="value: $root.users.editorPassword" required>
</div>
</div>
<div class="control-group" data-bind="css: {error: $root.users.editorPasswordMismatch()}">
<label class="control-label" for="settings-usersDialogAddUserPassword2">Repeat Password</label>
<div class="controls">
<input type="password" class="input-block-level" id="settings-usersDialogAddUserPassword2" data-bind="value: $root.users.editorRepeatedPassword, valueUpdate: 'afterkeydown'" required>
<span class="help-inline" data-bind="visible: $root.users.editorPasswordMismatch()">Passwords do not match</span>
</div>
</div>
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" id="settings-usersDialogAddUserActive" data-bind="checked: $root.users.editorActive"> Active
</label>
</div>
</div>
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" id="settings-usersDialogAddUserAdmin" data-bind="checked: $root.users.editorAdmin"> Admin
</label>
</div>
</div>
</form>
</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.confirmAddUser(); }, enable: !$root.users.editorPasswordMismatch()">Confirm</button>
</div>
</div>
<div id="settings-usersDialogEditUser" class="modal hide fade">
<div class="modal-header">
<a href="#" class="close" data-dismiss="modal" aria-hidden="true">&times;</a>
<h3>Edit user "<span data-bind="text: $root.users.editorUsername"></span>"</h3>
</div>
<div class="modal-body">
<form class="form-horizontal">
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" id="settings-usersDialogEditUserActive" data-bind="checked: $root.users.editorActive"> Active
</label>
</div>
</div>
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" id="settings-usersDialogEditUserAdmin" data-bind="checked: $root.users.editorAdmin"> Admin
</label>
</div>
</div>
</form>
</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>
</div>
</div>
<div id="settings-usersDialogChangePassword" class="modal hide fade">
<div class="modal-header">
<a href="#" class="close" data-dismiss="modal" aria-hidden="true">&times;</a>
<h3>Change password for user "<span data-bind="text: $root.users.editorUsername"></span>"</h3>
</div>
<div class="modal-body">
<form class="form-horizontal">
<div class="control-group">
<label class="control-label" for="settings-usersDialogChangePasswordPassword1">New Password</label>
<div class="controls">
<input type="password" class="input-block-level" id="settings-usersDialogChangePasswordPassword1" data-bind="value: $root.users.editorPassword" required>
</div>
</div>
<div class="control-group" data-bind="css: {error: $root.users.editorPasswordMismatch()}">
<label class="control-label" for="settings-usersDialogChangePasswordPassword2">Repeat Password</label>
<div class="controls">
<input type="password" class="input-block-level" id="settings-usersDialogChangePasswordPassword2" data-bind="value: $root.users.editorRepeatedPassword, valueUpdate: 'afterkeydown'" required>
<span class="help-inline" data-bind="visible: $root.users.editorPasswordMismatch()">Passwords do not match</span>
</div>
</div>
</form>
</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.confirmChangePassword(); }, enable: !$root.users.editorPasswordMismatch()">Confirm</button>
</div>
</div>
</div>
{% endif %}
</div> </div>
</div> </div>
</div> </div>

View File

@ -83,7 +83,7 @@ class Timelapse(object):
with self._captureMutex: with self._captureMutex:
filename = os.path.join(self._captureDir, "tmp_%05d.jpg" % (self._imageNumber)) filename = os.path.join(self._captureDir, "tmp_%05d.jpg" % (self._imageNumber))
self._imageNumber += 1; self._imageNumber += 1
self._logger.debug("Capturing image to %s" % filename) self._logger.debug("Capturing image to %s" % filename)
captureThread = threading.Thread(target=self._captureWorker, kwargs={"filename": filename}) captureThread = threading.Thread(target=self._captureWorker, kwargs={"filename": filename})

235
octoprint/users.py Normal file
View File

@ -0,0 +1,235 @@
# coding=utf-8
__author__ = "Gina Häußge <osd@foosel.net>"
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
from flask.ext.login import UserMixin
from flask.ext.principal import Identity
import hashlib
import os
import yaml
from octoprint.settings import settings
class UserManager(object):
valid_roles = ["user", "admin"]
@staticmethod
def createPasswordHash(password):
return hashlib.sha512(password + "mvBUTvwzBzD3yPwvnJ4E4tXNf3CGJvvW").hexdigest()
def addUser(self, username, password, active, roles):
pass
def changeUserActivation(self, username, active):
pass
def changeUserRoles(self, username, roles):
pass
def addRolesToUser(self, username, roles):
pass
def removeRolesFromUser(self, username, roles):
pass
def changeUserPassword(self, username, password):
pass
def removeUser(self, username):
pass
def findUser(self, username=None):
return None
def getAllUsers(self):
return []
##~~ FilebasedUserManager, takes available users from users.yaml file
class FilebasedUserManager(UserManager):
def __init__(self):
UserManager.__init__(self)
userfile = settings().get(["accessControl", "userfile"])
if userfile is None:
userfile = os.path.join(settings().settings_dir, "users.yaml")
self._userfile = userfile
self._users = {}
self._dirty = False
self._load()
def _load(self):
if os.path.exists(self._userfile) and os.path.isfile(self._userfile):
with open(self._userfile, "r") as f:
data = yaml.safe_load(f)
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:
return
data = {}
for name in self._users.keys():
user = self._users[name]
data[name] = {
"password": user._passwordHash,
"active": user._active,
"roles": user._roles
}
with open(self._userfile, "wb") as f:
yaml.safe_dump(data, f, default_flow_style=False, indent=" ", allow_unicode=True)
self._dirty = False
self._load()
def addUser(self, username, password, active=False, roles=["user"]):
if username in self._users.keys():
raise UserAlreadyExists(username)
self._users[username] = User(username, UserManager.createPasswordHash(password), active, roles)
self._dirty = True
self._save()
def changeUserActivation(self, username, active):
if not username in self._users.keys():
raise UnknownUser(username)
if self._users[username]._active != active:
self._users[username]._active = active
self._dirty = True
self._save()
def changeUserRoles(self, username, roles):
if not username in self._users.keys():
raise UnknownUser(username)
user = self._users[username]
removedRoles = set(user._roles) - set(roles)
self.removeRolesFromUser(username, removedRoles)
addedRoles = set(roles) - set(user._roles)
self.addRolesToUser(username, addedRoles)
def addRolesToUser(self, username, roles):
if not username in self._users.keys():
raise UnknownUser(username)
user = self._users[username]
for role in roles:
if not role in user._roles:
user._roles.append(role)
self._dirty = True
self._save()
def removeRolesFromUser(self, username, roles):
if not username in self._users.keys():
raise UnknownUser(username)
user = self._users[username]
for role in roles:
if role in user._roles:
user._roles.remove(role)
self._dirty = True
self._save()
def changeUserPassword(self, username, password):
if not username in self._users.keys():
raise UnknownUser(username)
passwordHash = UserManager.createPasswordHash(password)
user = self._users[username]
if user._passwordHash != passwordHash:
user._passwordHash = passwordHash
self._dirty = True
self._save()
def removeUser(self, username):
if not username in self._users.keys():
raise UnknownUser(username)
del self._users[username]
self._dirty = True
self._save()
def findUser(self, username=None):
if username is None:
return None
if username not in self._users.keys():
return None
return self._users[username]
def getAllUsers(self):
return map(lambda x: x.asDict(), self._users.values())
##~~ Exceptions
class UserAlreadyExists(Exception):
def __init__(self, username):
Exception.__init__(self, "User %s already exists" % username)
class UnknownUser(Exception):
def __init__(self, username):
Exception.__init__(self, "Unknown user: %s" % username)
class UnknownRole(Exception):
def _init_(self, role):
Exception.__init__(self, "Unknown role: %s" % role)
##~~ User object
class User(UserMixin):
def __init__(self, username, passwordHash, active, roles):
self._username = username
self._passwordHash = passwordHash
self._active = active
self._roles = roles
def asDict(self):
return {
"name": self._username,
"active": self.is_active(),
"admin": self.is_admin(),
"user": self.is_user()
}
def check_password(self, passwordHash):
return self._passwordHash == passwordHash
def get_id(self):
return self._username
def get_name(self):
return self._username
def is_active(self):
return self._active
def is_user(self):
return "user" in self._roles
def is_admin(self):
return "admin" in self._roles
##~~ DummyUser object to use when accessControl is disabled
class DummyUser(User):
def __init__(self):
User.__init__(self, "dummy", "", True, UserManager.valid_roles)
def check_password(self, passwordHash):
return True
class DummyIdentity(Identity):
def __init__(self):
Identity.__init__(self, "dummy")
def dummy_identity_loader():
return DummyIdentity()

View File

@ -28,3 +28,14 @@ def getFormattedDateTime(d):
return None return None
return d.strftime("%Y-%m-%d %H:%M") return d.strftime("%Y-%m-%d %H:%M")
def getClass(name):
"""
Taken from http://stackoverflow.com/a/452981/2028598
"""
parts = name.split(".")
module = ".".join(parts[:-1])
m = __import__(module)
for comp in parts[1:]:
m = getattr(m, comp)
return m

View File

@ -560,8 +560,6 @@ class MachineCom(object):
def _sendCommand(self, cmd, sendChecksum=False): def _sendCommand(self, cmd, sendChecksum=False):
# Make sure we are only handling one sending job at a time # Make sure we are only handling one sending job at a time
with self._sendingLock: with self._sendingLock:
cmd = cmd.upper()
if self._serial is None: if self._serial is None:
return return
if 'M109' in cmd or 'M190' in cmd: if 'M109' in cmd or 'M190' in cmd:

View File

@ -4,3 +4,5 @@ pyserial>=2.6
tornado>=2.4.1 tornado>=2.4.1
tornadio2>=0.0.4 tornadio2>=0.0.4
PyYAML>=3.10 PyYAML>=3.10
Flask-Login>=0.1.3
Flask-Principal>=0.3.5

2
run
View File

@ -1,4 +1,4 @@
#!/usr/bin/python #!/usr/bin/env python
import sys import sys
from octoprint.daemon import Daemon from octoprint.daemon import Daemon
from octoprint.server import Server from octoprint.server import Server