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>"
__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
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 threading
@ -12,23 +14,30 @@ import logging, logging.config
import subprocess
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.gcodefiles as gcodefiles
import octoprint.util as util
import octoprint.users as users
SUCCESS = {}
BASEURL = "/ajax/"
app = Flask("octoprint")
# Only instantiated by the Server().run() method
# In order that threads don't start too early when running as a Daemon
printer = None
gcodeManager = None
userManager = None
principals = Principal(app)
admin_permission = Permission(RoleNeed("admin"))
user_permission = Permission(RoleNeed("user"))
#~~ Printer state
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)
self._logger = logging.getLogger(__name__)
@ -40,6 +49,10 @@ class PrinterStateConnection(tornadio2.SocketConnection):
self._messageBacklog = []
self._messageBacklogMutex = threading.Lock()
self._printer = printer
self._gcodeManager = gcodeManager
self._userManager = userManager
def on_open(self, info):
self._logger.info("New connection from client")
# Use of global here is smelly
@ -99,40 +112,43 @@ class PrinterStateConnection(tornadio2.SocketConnection):
@app.route("/")
def index():
return render_template(
"index.html",
"index.jinja2",
ajaxBaseUrl=BASEURL,
webcamStream=settings().get(["webcam", "stream"]),
enableTimelapse=(settings().get(["webcam", "snapshot"]) is not None and settings().get(["webcam", "ffmpeg"]) is not None),
enableGCodeVisualizer=settings().get(["feature", "gCodeVisualizer"]),
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
@app.route(BASEURL + "control/connectionOptions", methods=["GET"])
@app.route(BASEURL + "control/connection/options", methods=["GET"])
def connectionOptions():
return jsonify(getConnectionOptions())
@app.route(BASEURL + "control/connect", methods=["POST"])
@app.route(BASEURL + "control/connection", methods=["POST"])
@login_required
def connect():
port = None
baudrate = None
if "port" in request.values.keys():
port = request.values["port"]
if "baudrate" in request.values.keys():
baudrate = request.values["baudrate"]
if "save" in request.values.keys():
settings().set(["serial", "port"], port)
settings().setInt(["serial", "baudrate"], baudrate)
settings().save()
printer.connect(port=port, baudrate=baudrate)
return jsonify(state="Connecting")
if "command" in request.values.keys() and request.values["command"] == "connect":
port = None
baudrate = None
if "port" in request.values.keys():
port = request.values["port"]
if "baudrate" in request.values.keys():
baudrate = request.values["baudrate"]
if "save" in request.values.keys():
settings().set(["serial", "port"], port)
settings().setInt(["serial", "baudrate"], baudrate)
settings().save()
printer.connect(port=port, baudrate=baudrate)
elif "command" in request.values.keys() and request.values["command"] == "disconnect":
printer.disconnect()
@app.route(BASEURL + "control/disconnect", methods=["POST"])
def disconnect():
printer.disconnect()
return jsonify(state="Offline")
return jsonify(SUCCESS)
@app.route(BASEURL + "control/command", methods=["POST"])
@login_required
def printerCommand():
if "application/json" in request.headers["Content-Type"]:
data = request.json
@ -155,32 +171,27 @@ def printerCommand():
return jsonify(SUCCESS)
@app.route(BASEURL + "control/print", methods=["POST"])
def printGcode():
printer.startPrint()
return jsonify(SUCCESS)
@app.route(BASEURL + "control/pause", methods=["POST"])
def pausePrint():
printer.togglePausePrint()
return jsonify(SUCCESS)
@app.route(BASEURL + "control/cancel", methods=["POST"])
def cancelPrint():
printer.cancelPrint()
@app.route(BASEURL + "control/job", methods=["POST"])
@login_required
def printJobControl():
if "command" in request.values.keys():
if request.values["command"] == "start":
printer.startPrint()
elif request.values["command"] == "pause":
printer.togglePausePrint()
elif request.values["command"] == "cancel":
printer.cancelPrint()
return jsonify(SUCCESS)
@app.route(BASEURL + "control/temperature", methods=["POST"])
@login_required
def setTargetTemperature():
if not printer.isOperational():
return jsonify(SUCCESS)
elif request.values.has_key("temp"):
# set target temperature
if "temp" in request.values.keys():
# set target temperature
temp = request.values["temp"]
printer.command("M104 S" + temp)
elif request.values.has_key("bedTemp"):
if "bedTemp" in request.values.keys():
# set target bed temperature
bedTemp = request.values["bedTemp"]
printer.command("M140 S" + bedTemp)
@ -188,6 +199,7 @@ def setTargetTemperature():
return jsonify(SUCCESS)
@app.route(BASEURL + "control/jog", methods=["POST"])
@login_required
def jog():
if not printer.isOperational() or printer.isPrinting():
# 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())
@app.route(BASEURL + "control/speed", methods=["POST"])
@login_required
def speed():
if not printer.isOperational():
return jsonify(SUCCESS)
@ -251,6 +264,7 @@ def readGcodeFile(filename):
return send_from_directory(settings().getBaseFolder("uploads"), filename, as_attachment=True)
@app.route(BASEURL + "gcodefiles/upload", methods=["POST"])
@login_required
def uploadGcodeFile():
filename = None
if "gcode_file" in request.files.keys():
@ -259,10 +273,11 @@ def uploadGcodeFile():
return jsonify(files=gcodeManager.getAllFileData(), filename=filename)
@app.route(BASEURL + "gcodefiles/load", methods=["POST"])
@login_required
def loadGcodeFile():
if "filename" in request.values.keys():
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
filename = gcodeManager.getAbsolutePath(request.values["filename"])
if filename is not None:
@ -270,6 +285,7 @@ def loadGcodeFile():
return jsonify(SUCCESS)
@app.route(BASEURL + "gcodefiles/delete", methods=["POST"])
@login_required
def deleteGcodeFile():
if "filename" in request.values.keys():
filename = request.values["filename"]
@ -308,6 +324,7 @@ def downloadTimelapse(filename):
return send_from_directory(settings().getBaseFolder("timelapse"), filename, as_attachment=True)
@app.route(BASEURL + "timelapse/<filename>", methods=["DELETE"])
@login_required
def deleteTimelapse(filename):
if util.isAllowedFile(filename, set(["mpg"])):
secure = os.path.join(settings().getBaseFolder("timelapse"), secure_filename(filename))
@ -315,7 +332,8 @@ def deleteTimelapse(filename):
os.remove(secure)
return getTimelapseData()
@app.route(BASEURL + "timelapse/config", methods=["POST"])
@app.route(BASEURL + "timelapse", methods=["POST"])
@login_required
def setTimelapseConfig():
if request.values.has_key("type"):
type = request.values["type"]
@ -381,6 +399,8 @@ def getSettings():
})
@app.route(BASEURL + "settings", methods=["POST"])
@login_required
@admin_permission.require(403)
def setSettings():
if "application/json" in request.headers["Content-Type"]:
data = request.json
@ -425,9 +445,117 @@ def setSettings():
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
@app.route(BASEURL + "system", methods=["POST"])
@login_required
@admin_permission.require(403)
def performSystemAction():
logger = logging.getLogger(__name__)
if request.values.has_key("action"):
@ -446,6 +574,61 @@ def performSystemAction():
return app.make_response(("Command failed: %r" % ex, 500, []))
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
class Server():
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 printer
global gcodeManager
global userManager
from tornado.wsgi import WSGIContainer
from tornado.httpserver import HTTPServer
@ -470,19 +654,37 @@ class Server():
# then initialize logging
self._initLogging(self._debug)
logger = logging.getLogger(__name__)
gcodeManager = gcodefiles.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:
self._host = settings().get(["server", "host"])
if self._port is None:
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
self._router = tornadio2.TornadioRouter(PrinterStateConnection)
self._router = tornadio2.TornadioRouter(self._createSocketConnection)
self._tornado_app = Application(self._router.urls + [
(".*", FallbackHandler, {"fallback": WSGIContainer(app)})
@ -491,11 +693,15 @@ class Server():
self._server.listen(self._port, address=self._host)
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):
s = settings(init=True, basedir=basedir, configfile=configfile)
def _initLogging(self, debug):
self._config = {
config = {
"version": 1,
"formatters": {
"simple": {
@ -534,13 +740,13 @@ class Server():
}
if debug:
self._config["loggers"]["SERIAL"] = {
config["loggers"]["SERIAL"] = {
"level": "DEBUG",
"handlers": ["serialFile"],
"propagate": False
}
logging.config.dictConfig(self._config)
logging.config.dictConfig(config)
if __name__ == "__main__":
octoprint = Server()

View File

@ -72,6 +72,11 @@ default_settings = {
"controls": [],
"system": {
"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 {
.settings-trigger {
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 {
@ -181,6 +194,42 @@ table {
&.timelapse_files_action {
text-align: center;
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;
.jog-panel {
@ -324,7 +373,6 @@ ul.dropdown-menu li a {
/** Settings dialog */
#settings_dialog {
width: 650px;
}
/** 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.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.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;
}
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>");
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:
define([
'jquery',
'jquery.ui.widget'
'jquery.ui.widget.js'
], factory);
} else {
// Browser globals:

View File

@ -1,8 +1,99 @@
//~~ View models
function ConnectionViewModel() {
function LoginStateViewModel() {
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.baudrateOptions = ko.observableArray(undefined);
self.selectedPort = ko.observable(undefined);
@ -28,7 +119,7 @@ function ConnectionViewModel() {
self.requestData = function() {
$.ajax({
url: AJAX_BASEURL + "control/connectionOptions",
url: AJAX_BASEURL + "control/connection/options",
method: "GET",
dataType: "json",
success: function(response) {
@ -83,6 +174,7 @@ function ConnectionViewModel() {
self.connect = function() {
if (self.isErrorOrClosed()) {
var data = {
"command": "connect",
"port": self.selectedPort(),
"baudrate": self.selectedBaudrate()
};
@ -91,7 +183,7 @@ function ConnectionViewModel() {
data["save"] = true;
$.ajax({
url: AJAX_BASEURL + "control/connect",
url: AJAX_BASEURL + "control/connection",
type: "POST",
dataType: "json",
data: data
@ -99,17 +191,20 @@ function ConnectionViewModel() {
} else {
self.requestData();
$.ajax({
url: AJAX_BASEURL + "control/disconnect",
url: AJAX_BASEURL + "control/connection",
type: "POST",
dataType: "json"
dataType: "json",
data: {"command": "disconnect"}
})
}
}
}
function PrinterStateViewModel() {
function PrinterStateViewModel(loginStateViewModel) {
var self = this;
self.loginState = loginStateViewModel;
self.stateString = ko.observable(undefined);
self.isErrorOrClosed = ko.observable(undefined);
self.isOperational = ko.observable(undefined);
@ -200,11 +295,45 @@ function PrinterStateViewModel() {
self._processZData = function(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;
self.loginState = loginStateViewModel;
self.temp = ko.observable(undefined);
self.bedTemp = ko.observable(undefined);
self.targetTemp = ko.observable(undefined);
@ -376,9 +505,11 @@ function TemperatureViewModel(settingsViewModel) {
}
}
function ControlsViewModel() {
function ControlViewModel(loginStateViewModel) {
var self = this;
self.loginState = loginStateViewModel;
self.isErrorOrClosed = ko.observable(undefined);
self.isOperational = ko.observable(undefined);
self.isPrinting = ko.observable(undefined);
@ -531,66 +662,10 @@ function ControlsViewModel() {
}
function SpeedViewModel() {
function TerminalViewModel(loginStateViewModel) {
var self = this;
self.outerWall = ko.observable(undefined);
self.innerWall = ko.observable(undefined);
self.fill = ko.observable(undefined);
self.support = ko.observable(undefined);
self.isErrorOrClosed = ko.observable(undefined);
self.isOperational = ko.observable(undefined);
self.isPrinting = ko.observable(undefined);
self.isPaused = ko.observable(undefined);
self.isError = ko.observable(undefined);
self.isReady = ko.observable(undefined);
self.isLoading = ko.observable(undefined);
self._fromCurrentData = function(data) {
self._processStateData(data.state);
}
self._fromHistoryData = function(data) {
self._processStateData(data.state);
}
self._processStateData = function(data) {
self.isErrorOrClosed(data.flags.closedOrError);
self.isOperational(data.flags.operational);
self.isPaused(data.flags.paused);
self.isPrinting(data.flags.printing);
self.isError(data.flags.error);
self.isReady(data.flags.ready);
self.isLoading(data.flags.loading);
}
self.requestData = function() {
$.ajax({
url: AJAX_BASEURL + "control/speed",
type: "GET",
dataType: "json",
success: self._fromResponse
});
}
self._fromResponse = function(response) {
if (response.feedrate) {
self.outerWall(response.feedrate.outerWall);
self.innerWall(response.feedrate.innerWall);
self.fill(response.feedrate.fill);
self.support(response.feedrate.support);
} else {
self.outerWall(undefined);
self.innerWall(undefined);
self.fill(undefined);
self.support(undefined);
}
}
}
function TerminalViewModel() {
var self = this;
self.loginState = loginStateViewModel;
self.log = [];
@ -655,9 +730,11 @@ function TerminalViewModel() {
}
}
function GcodeFilesViewModel() {
function GcodeFilesViewModel(loginStateViewModel) {
var self = this;
self.loginState = loginStateViewModel;
self.isErrorOrClosed = ko.observable(undefined);
self.isOperational = ko.observable(undefined);
self.isPrinting = ko.observable(undefined);
@ -700,11 +777,11 @@ function GcodeFilesViewModel() {
);
self.isLoadActionPossible = ko.computed(function() {
return !self.isPrinting() && !self.isPaused() && !self.isLoading();
return self.loginState.isUser() && !self.isPrinting() && !self.isPaused() && !self.isLoading();
});
self.isLoadAndPrintActionPossible = ko.computed(function() {
return self.isOperational() && self.isLoadActionPossible();
return self.loginState.isUser() && self.isOperational() && self.isLoadActionPossible();
});
self.fromCurrentData = function(data) {
@ -789,9 +866,11 @@ function GcodeFilesViewModel() {
}
function WebcamViewModel() {
function TimelapseViewModel(loginStateViewModel) {
var self = this;
self.loginState = loginStateViewModel;
self.timelapseType = ko.observable(undefined);
self.timelapseTimedInterval = ko.observable(undefined);
@ -848,7 +927,6 @@ function WebcamViewModel() {
dataType: "json",
success: self.fromResponse
});
$("#webcam_image").attr("src", CONFIG_WEBCAM_STREAM + "?" + new Date().getTime());
}
self.fromResponse = function(response) {
@ -909,9 +987,11 @@ function WebcamViewModel() {
}
}
function GcodeViewModel() {
function GcodeViewModel(loginStateViewModel) {
var self = this;
self.loginState = loginStateViewModel;
self.loadedFilename = undefined;
self.status = 'idle';
self.enabled = false;
@ -972,9 +1052,202 @@ function GcodeViewModel() {
}
function SettingsViewModel() {
function UsersViewModel(loginStateViewModel) {
var self = this;
self.loginState = loginStateViewModel;
// initialize list helper
self.listHelper = new ItemListHelper(
"users",
{
"name": function(a, b) {
// sorts ascending
if (a["name"].toLocaleLowerCase() < b["name"].toLocaleLowerCase()) return -1;
if (a["name"].toLocaleLowerCase() > b["name"].toLocaleLowerCase()) return 1;
return 0;
}
},
{},
"name",
[],
CONFIG_USERSPERPAGE
);
self.emptyUser = {name: "", admin: false, active: false};
self.currentUser = ko.observable(self.emptyUser);
self.editorUsername = ko.observable(undefined);
self.editorPassword = ko.observable(undefined);
self.editorRepeatedPassword = ko.observable(undefined);
self.editorAdmin = ko.observable(undefined);
self.editorActive = ko.observable(undefined);
self.currentUser.subscribe(function(newValue) {
if (newValue === undefined) {
self.editorUsername(undefined);
self.editorAdmin(undefined);
self.editorActive(undefined);
} else {
self.editorUsername(newValue.name);
self.editorAdmin(newValue.admin);
self.editorActive(newValue.active);
}
self.editorPassword(undefined);
self.editorRepeatedPassword(undefined);
});
self.editorPasswordMismatch = ko.computed(function() {
return self.editorPassword() != self.editorRepeatedPassword();
});
self.requestData = function() {
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_color = ko.observable(undefined);
@ -1020,7 +1293,7 @@ function SettingsViewModel() {
type: "GET",
dataType: "json",
success: self.fromResponse
})
});
}
self.fromResponse = function(response) {
@ -1107,11 +1380,13 @@ function SettingsViewModel() {
}
function NavigationViewModel(appearanceViewModel, settingsViewModel) {
function NavigationViewModel(loginStateViewModel, appearanceViewModel, settingsViewModel, usersViewModel) {
var self = this;
self.loginState = loginStateViewModel;
self.appearance = appearanceViewModel;
self.systemActions = settingsViewModel.system_actions;
self.users = usersViewModel;
self.triggerAction = function(action) {
var callback = function() {
@ -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;
self.loginStateViewModel = loginStateViewModel;
self.connectionViewModel = connectionViewModel;
self.printerStateViewModel = printerStateViewModel;
self.temperatureViewModel = temperatureViewModel;
self.controlsViewModel = controlsViewModel;
self.controlViewModel = controlViewModel;
self.terminalViewModel = terminalViewModel;
self.speedViewModel = speedViewModel;
self.gcodeFilesViewModel = gcodeFilesViewModel;
self.webcamViewModel = webcamViewModel;
self.timelapseViewModel = timelapseViewModel;
self.gcodeViewModel = gcodeViewModel;
self._socket = io.connect();
self._socket.on("connect", function() {
if ($("#offline_overlay").is(":visible")) {
$("#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() {
@ -1177,9 +1454,9 @@ function DataUpdater(connectionViewModel, printerStateViewModel, temperatureView
self.connectionViewModel.fromHistoryData(data);
self.printerStateViewModel.fromHistoryData(data);
self.temperatureViewModel.fromHistoryData(data);
self.controlsViewModel.fromHistoryData(data);
self.controlViewModel.fromHistoryData(data);
self.terminalViewModel.fromHistoryData(data);
self.webcamViewModel.fromHistoryData(data);
self.timelapseViewModel.fromHistoryData(data);
self.gcodeViewModel.fromHistoryData(data);
self.gcodeFilesViewModel.fromCurrentData(data);
})
@ -1187,9 +1464,9 @@ function DataUpdater(connectionViewModel, printerStateViewModel, temperatureView
self.connectionViewModel.fromCurrentData(data);
self.printerStateViewModel.fromCurrentData(data);
self.temperatureViewModel.fromCurrentData(data);
self.controlsViewModel.fromCurrentData(data);
self.controlViewModel.fromCurrentData(data);
self.terminalViewModel.fromCurrentData(data);
self.webcamViewModel.fromCurrentData(data);
self.timelapseViewModel.fromCurrentData(data);
self.gcodeViewModel.fromCurrentData(data);
self.gcodeFilesViewModel.fromCurrentData(data);
})
@ -1451,36 +1728,38 @@ function AppearanceViewModel(settingsViewModel) {
$(function() {
//~~ View models
var connectionViewModel = new ConnectionViewModel();
var printerStateViewModel = new PrinterStateViewModel();
var settingsViewModel = new SettingsViewModel();
var loginStateViewModel = new LoginStateViewModel(loginStateViewModel);
var usersViewModel = new UsersViewModel(loginStateViewModel);
var connectionViewModel = new ConnectionViewModel(loginStateViewModel);
var printerStateViewModel = new PrinterStateViewModel(loginStateViewModel);
var settingsViewModel = new SettingsViewModel(loginStateViewModel, usersViewModel);
var appearanceViewModel = new AppearanceViewModel(settingsViewModel);
var temperatureViewModel = new TemperatureViewModel(settingsViewModel);
var controlsViewModel = new ControlsViewModel();
var speedViewModel = new SpeedViewModel();
var terminalViewModel = new TerminalViewModel();
var gcodeFilesViewModel = new GcodeFilesViewModel();
var webcamViewModel = new WebcamViewModel();
var gcodeViewModel = new GcodeViewModel();
var navigationViewModel = new NavigationViewModel(appearanceViewModel, settingsViewModel);
var temperatureViewModel = new TemperatureViewModel(loginStateViewModel, settingsViewModel);
var controlViewModel = new ControlViewModel(loginStateViewModel);
var terminalViewModel = new TerminalViewModel(loginStateViewModel);
var gcodeFilesViewModel = new GcodeFilesViewModel(loginStateViewModel);
var timelapseViewModel = new TimelapseViewModel(loginStateViewModel);
var gcodeViewModel = new GcodeViewModel(loginStateViewModel);
var navigationViewModel = new NavigationViewModel(loginStateViewModel, appearanceViewModel, settingsViewModel, usersViewModel);
var dataUpdater = new DataUpdater(
loginStateViewModel,
connectionViewModel,
printerStateViewModel,
temperatureViewModel,
controlsViewModel,
speedViewModel,
controlViewModel,
terminalViewModel,
gcodeFilesViewModel,
webcamViewModel,
timelapseViewModel,
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({
type: 'POST',
headers: { "cache-control": "no-cache" }
});
type: 'POST',
headers: { "cache-control": "no-cache" }
});
//~~ Show settings - to ensure centered
$('#navbar_show_settings').click(function() {
@ -1492,31 +1771,7 @@ $(function() {
return false;
})
//~~ Print job control
$("#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"
})
})
//~~ Print job control (should move to PrinterStateViewModel)
//~~ Temperature control (should really move to knockout click binding)
@ -1542,34 +1797,22 @@ $(function() {
})
$('#tabs a[data-toggle="tab"]').on('shown', function (e) {
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-send").click(function () {
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) {
$.ajax({
url: AJAX_BASEURL + "control/command",
@ -1613,23 +1856,6 @@ $(function() {
//~~ Offline overlay
$("#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
ko.bindingHandlers.popover = {
@ -1649,34 +1875,46 @@ $(function() {
}
}
ko.applyBindings(connectionViewModel, document.getElementById("connection"));
ko.applyBindings(printerStateViewModel, document.getElementById("state"));
ko.applyBindings(gcodeFilesViewModel, document.getElementById("files"));
ko.applyBindings(gcodeFilesViewModel, document.getElementById("files-heading"));
ko.applyBindings(connectionViewModel, document.getElementById("connection_accordion"));
ko.applyBindings(printerStateViewModel, document.getElementById("state_accordion"));
ko.applyBindings(gcodeFilesViewModel, document.getElementById("files_accordion"));
ko.applyBindings(temperatureViewModel, document.getElementById("temp"));
ko.applyBindings(controlsViewModel, document.getElementById("controls"));
ko.applyBindings(controlViewModel, document.getElementById("control"));
ko.applyBindings(terminalViewModel, document.getElementById("term"));
ko.applyBindings(speedViewModel, document.getElementById("speed"));
ko.applyBindings(gcodeViewModel, document.getElementById("gcode"));
ko.applyBindings(settingsViewModel, document.getElementById("settings_dialog"));
ko.applyBindings(navigationViewModel, document.getElementById("navbar"));
ko.applyBindings(appearanceViewModel, document.getElementsByTagName("head")[0]);
var webcamElement = document.getElementById("webcam");
if (webcamElement) {
ko.applyBindings(webcamViewModel, document.getElementById("webcam"));
var timelapseElement = document.getElementById("timelapse");
if (timelapseElement) {
ko.applyBindings(timelapseViewModel, timelapseElement);
}
var gCodeVisualizerElement = document.getElementById("gcode");
if(gCodeVisualizerElement){
if (gCodeVisualizerElement) {
gcodeViewModel.initialize();
}
//~~ startup commands
loginStateViewModel.requestData();
connectionViewModel.requestData();
controlsViewModel.requestData();
controlViewModel.requestData();
gcodeFilesViewModel.requestData();
webcamViewModel.requestData();
settingsViewModel.requestData();
timelapseViewModel.requestData();
loginStateViewModel.subscribe(function(change, data) {
if ("login" == change) {
$("#gcode_upload").fileupload("enable");
settingsViewModel.requestData();
if (data.admin) {
usersViewModel.requestData();
}
} else {
$("#gcode_upload").fileupload("disable");
}
})
//~~ UI stuff
@ -1692,6 +1930,16 @@ $(function() {
$.pnotify.defaults.history = false;
$.fn.modal.defaults.maxHeight = function(){
// subtract the height of the modal header and footer
return $(window).height() - 165;
}
// Fix input element click problem on login dialog
$('.dropdown input, .dropdown label').click(function(e) {
e.stopPropagation();
});
}
);

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 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/jquery.fileupload-ui.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">
<script lang="javascript">
var AJAX_BASEURL = "/ajax/";
var AJAX_BASEURL = "{{ ajaxBaseUrl }}";
var CONFIG_GCODEFILESPERPAGE = 5;
var CONFIG_TIMELAPSEFILESPERPAGE = 10;
var CONFIG_USERSPERPAGE = 10;
var CONFIG_WEBCAM_STREAM = "{{ webcamStream }}";
var CONFIG_ACCESS_CONTROL = {% if enableAccessControl -%} true; {% else %} false; {%- endif %}
var WEB_SOCKET_SWF_LOCATION = "{{ url_for('static', filename='js/WebSocketMain.swf') }}";
var WEB_SOCKET_SWF_LOCATION = "{{ url_for('static', filename='js/socket.io/WebSocketMain.swf') }}";
var WEB_SOCKET_DEBUG = true;
</script>
<script src="{{ url_for('static', filename='js/less-1.3.3.min.js') }}" type="text/javascript"></script>
@ -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>
<div class="nav-collapse">
<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 %}
<li class="dropdown">
<li class="dropdown" style="display: none" data-bind="visible: loginState.isAdmin">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<i class="icon-off"></i> System
<b class="caret"></b>
@ -45,6 +53,28 @@
</ul>
</li>
{% 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>
</div>
</div>
@ -53,24 +83,24 @@
<div class="container octoprint-container">
<div class="row">
<div class="accordion span4">
<div class="accordion-group">
<div class="accordion-group" data-bind="visible: loginState.isUser" id="connection_accordion">
<div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" href="#connection"><i class="icon-signal"></i> Connection</a>
</div>
<div class="accordion-body collapse in" id="connection">
<div class="accordion-inner">
<label for="connection_ports" data-bind="css: {disabled: !isErrorOrClosed}, enable: isErrorOrClosed">Serial Port</label>
<select id="connection_ports" data-bind="options: portOptions, optionsCaption: 'AUTO', value: selectedPort, css: {disabled: !isErrorOrClosed}, enable: isErrorOrClosed"></select>
<label for="connection_baudrates" data-bind="css: {disabled: !isErrorOrClosed}, enable: isErrorOrClosed">Baudrate</label>
<select id="connection_baudrates" data-bind="options: baudrateOptions, optionsCaption: 'AUTO', value: selectedBaudrate, css: {disabled: !isErrorOrClosed}, enable: isErrorOrClosed"></select>
<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 && loginState.isUser"></select>
<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 && loginState.isUser"></select>
<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>
<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 class="accordion-group">
<div class="accordion-group" id="state_accordion">
<div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" href="#state"><i class="icon-info-sign"></i> State</a>
</div>
@ -89,21 +119,21 @@
<div class="bar" id="job_progressBar" data-bind="style: { width: progress() + '%' }"></div>
</div>
<div class="row-fluid print-control">
<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 span4" id="job_pause" data-bind="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>
<div class="row-fluid print-control" style="display: none;" data-bind="visible: loginState.isUser">
<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="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" data-bind="click: cancel, enable: isOperational() && (isPrinting() || isPaused()) && loginState.isUser()"><i class="icon-stop"></i> Cancel</button>
</div>
</div>
</div>
</div>
<div class="accordion-group">
<div class="accordion-heading" id="files-heading">
<div class="accordion-group" id="files_accordion">
<div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" href="#files"><i class="icon-list"></i> Files</a>
<div class="settings-trigger btn-group">
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
<i class="icon-wrench"></i>
<span class="icon-wrench"></span>
</a>
<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>
@ -129,7 +159,7 @@
<td class="gcode_files_name" data-bind="text: name"></td>
<td class="gcode_files_size" data-bind="text: size"></td>
<td class="gcode_files_action">
<a href="#" class="icon-trash" title="Remove" data-bind="click: function() { $root.removeFile($data.name); }"></a>&nbsp;|&nbsp;<a href="#" class="icon-folder-open" title="Load" data-bind="click: function() { if ($root.isLoadActionPossible()) { $root.loadFile($data.name, false); } else { return; } }, css: {disabled: !$root.isLoadActionPossible()}"></a>&nbsp;|&nbsp;<a href="#" class="icon-print" title="Load and Print" data-bind="click: function() { if ($root.isLoadAndPrintActionPossible()) { $root.loadFile($data.name, true); } else { return; } }, css: {disabled: !$root.isLoadAndPrintActionPossible()}"></a>
<a href="#" class="icon-trash" title="Remove" data-bind="click: function() { if ($root.loginState.isUser()) { $root.removeFile($data.name); } else { return; } }, css: {disabled: !$root.loginState.isUser()}"></a>&nbsp;|&nbsp;<a href="#" class="icon-folder-open" title="Load" data-bind="click: function() { if ($root.isLoadActionPossible()) { $root.loadFile($data.name, false); } else { return; } }, css: {disabled: !$root.isLoadActionPossible()}"></a>&nbsp;|&nbsp;<a href="#" class="icon-print" title="Load and Print" data-bind="click: function() { if ($root.isLoadAndPrintActionPossible()) { $root.loadFile($data.name, true); } else { return; } }, css: {disabled: !$root.isLoadAndPrintActionPossible()}"></a>
</td>
</tr>
</tbody>
@ -145,30 +175,31 @@
<li data-bind="css: {disabled: listHelper.currentPage() === listHelper.lastPage()}"><a href="#" data-bind="click: listHelper.nextPage">»</a></li>
</ul>
</div>
<span class="btn btn-primary btn-block fileinput-button" style="margin-bottom: 10px">
<i class="icon-upload icon-white"></i>
<span>Upload</span>
<input id="gcode_upload" type="file" name="gcode_file" class="fileinput-button" data-url="/ajax/gcodefiles/upload">
</span>
<div id="gcode_upload_progress" class="progress" style="width: 100%;">
<div class="bar" style="width: 0%"></div>
</div>
<div>
<small>Hint: You can also drag and drop files on this page to upload them.</small>
<div style="display: none;" data-bind="visible: loginState.isUser">
<span class="btn btn-primary btn-block fileinput-button" data-bind="css: {disabled: !$root.loginState.isUser()}" style="margin-bottom: 10px">
<i class="icon-upload icon-white"></i>
<span>Upload</span>
<input id="gcode_upload" type="file" name="gcode_file" class="fileinput-button" data-url="/ajax/gcodefiles/upload" data-bind="enable: loginState.isUser()">
</span>
<div id="gcode_upload_progress" class="progress" style="width: 100%;">
<div class="bar" style="width: 0%"></div>
</div>
<div>
<small>Hint: You can also drag and drop files on this page to upload them.</small>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="tabbable span8">
<div class="span8 tabbable">
<ul class="nav nav-tabs" id="tabs">
<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 %}
<!--<li><a href="#speed" data-toggle="tab">Speed</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>
<div class="tab-content">
@ -184,27 +215,29 @@
<label>Target: <strong data-bind="html: targetTempString"></strong></label>
<label for="temp_newTemp">New Target</label>
<div class="input-append">
<input type="text" id="temp_newTemp" data-bind="attr: {placeholder: targetTemp}" class="tempInput">
<span class="add-on">&deg;C</span>
</div>
<div class="btn-group">
<button type="submit" class="btn" id="temp_newTemp_set">Set</button>
<button class="btn dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<!-- ko foreach: temperature_profiles -->
<li>
<a href="#" data-bind="click: $parent.setTempFromProfile, text: 'Set ' + name + ' (' + extruder + '&deg;C)'"></a>
</li>
<!-- /ko -->
<li class="divider"></li>
<li>
<a href="#" data-bind="click: function() { $root.setTemp(0); }">Off</a>
</li>
</ul>
<div style="display: none;" data-bind="visible: loginState.isUser">
<label for="temp_newTemp">New Target</label>
<div class="input-append">
<input type="text" id="temp_newTemp" data-bind="attr: {placeholder: targetTemp}, enable: isOperational() && loginState.isUser()" class="tempInput">
<span class="add-on">&deg;C</span>
</div>
<div class="btn-group">
<button type="submit" class="btn" id="temp_newTemp_set" data-bind="enable: isOperational() && loginState.isUser()">Set</button>
<button class="btn dropdown-toggle" data-toggle="dropdown" data-bind="enable: isOperational() && loginState.isUser()">
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<!-- ko foreach: temperature_profiles -->
<li>
<a href="#" data-bind="click: $parent.setTempFromProfile, text: 'Set ' + name + ' (' + extruder + '&deg;C)'"></a>
</li>
<!-- /ko -->
<li class="divider"></li>
<li>
<a href="#" data-bind="click: function() { $root.setTemp(0); }">Off</a>
</li>
</ul>
</div>
</div>
</div>
<div class="form-horizontal span6">
@ -214,95 +247,103 @@
<label>Target: <strong data-bind="html: bedTargetTempString"></strong></label>
<label for="temp_newBedTemp">New Target</label>
<div class="input-append">
<input type="text" id="temp_newBedTemp" data-bind="attr: {placeholder: bedTargetTemp}" class="tempInput">
<span class="add-on">&deg;C</span>
</div>
<div class="btn-group">
<button type="submit" class="btn" id="temp_newBedTemp_set">Set</button>
<button class="btn dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<!-- ko foreach: temperature_profiles -->
<li>
<a href="#" data-bind="click: $parent.setBedTempFromProfile, text: 'Set ' + name + ' (' + bed + '&deg;C)'"></a>
</li>
<!-- /ko -->
<li class="divider"></li>
<li>
<a href="#" data-bind="click: function(){ $root.setBedTemp(0); }">Off</a>
</li>
</ul>
<div style="display: none;" data-bind="visible: loginState.isUser">
<label for="temp_newBedTemp">New Target</label>
<div class="input-append">
<input type="text" id="temp_newBedTemp" data-bind="attr: {placeholder: bedTargetTemp}, enable: isOperational() && loginState.isUser()" class="tempInput">
<span class="add-on">&deg;C</span>
</div>
<div class="btn-group">
<button type="submit" class="btn" id="temp_newBedTemp_set" data-bind="enable: isOperational() && loginState.isUser()">Set</button>
<button class="btn dropdown-toggle" data-toggle="dropdown" data-bind="enable: isOperational() && loginState.isUser()">
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<!-- ko foreach: temperature_profiles -->
<li>
<a href="#" data-bind="click: $parent.setBedTempFromProfile, text: 'Set ' + name + ' (' + bed + '&deg;C)'"></a>
</li>
<!-- /ko -->
<li class="divider"></li>
<li>
<a href="#" data-bind="click: function(){ $root.setBedTemp(0); }">Off</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<div class="tab-pane" id="controls">
<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 -->
<div class="jog-panel">
<h1>X/Y</h1>
<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>
<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(), 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-left"></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() && loginState.isUser(), click: function() { $root.sendJogCommand('x',1) }"><i class="icon-arrow-right"></i></button>
</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>
<!-- Z jogging control panel -->
<div class="jog-panel">
<h1>Z</h1>
<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>
<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>
<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>
<!-- Jog distance -->
<div class="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="1">1</button>
<button type="button" class="btn active" data-distance="10">10</button>
<button type="button" class="btn" data-distance="100">100</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" data-bind="enable: loginState.isUser()">1</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" data-bind="enable: loginState.isUser()">100</button>
</div>
</div>
</div>
<!-- Extrusion control panel -->
<div class="jog-panel">
<div class="jog-panel" style="display: none;" data-bind="visible: loginState.isUser">
<h1>E</h1>
<div>
<div class="input-append control-box">
<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>
</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(), click: function() { $root.sendRetractCommand() }">Retract</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() && loginState.isUser(), click: function() { $root.sendRetractCommand() }">Retract</button>
</div>
</div>
<!-- General control panel -->
<div class="jog-panel">
<div class="jog-panel" style="display: none;" data-bind="visible: loginState.isUser">
<h1>General</h1>
<div>
<button class="btn control-box" data-bind="enable: isOperational() && !isPrinting(), 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(), click: function() { $root.sendCustomCommand({type:'command',command:'M106 S0'}) }">Fans 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() && loginState.isUser(), 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 S0'}) }">Fans off</button>
</div>
</div>
<!-- Container for custom controls -->
<div style="clear: both;" data-bind="template: { name: $root.displayMode, foreach: controls }"></div>
<div style="clear: both; display: none;" data-bind="visible: loginState.isUser, template: { name: $root.displayMode, foreach: controls }"></div>
<!-- Templates for custom controls -->
<script type="text/html" id="customControls_sectionTemplate">
@ -312,7 +353,7 @@
</script>
<script type="text/html" id="customControls_commandTemplate">
<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>
</script>
<script type="text/html" id="customControls_parametricCommandTemplate">
@ -321,43 +362,12 @@
<label data-bind="text: name"></label>
<input type="text" class="input-small" data-bind="attr: {placeholder: name}, value: value">
<!-- /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>
</script>
<script type="text/html" id="customControls_emptyTemplate"><div></div></script>
<!-- End of templates for custom controls -->
</div>
<div class="tab-pane" id="speed">
<div class="form-horizontal" style="margin-bottom: 20px">
<label for="speed_outerWall">Outer Wall</label>
<div class="input-append">
<input type="text" id="speed_outerWall" class="input-mini" data-bind="enable: isOperational(), 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">
<canvas id="canvas" width="572" height="588"></canvas>
<div id="slider-vertical"></div>
@ -451,37 +461,34 @@
<input type="checkbox" id="terminal-autoscroll" data-bind="checked: autoscrollEnabled"> Autoscroll
</label>
<div class="input-append">
<input type="text" id="terminal-command">
<button class="btn" type="button" id="terminal-send">Send</button>
<div class="input-append" style="display: none;" data-bind="visible: loginState.isUser">
<input type="text" id="terminal-command" data-bind="enable: isOperational() && loginState.isUser()">
<button class="btn" type="button" id="terminal-send" data-bind="enable: isOperational() && loginState.isUser()">Send</button>
</div>
</div>
{% if webcamStream %}
<div class="tab-pane" id="webcam">
<div id="webcam_container">
<img id="webcam_image" src="{{ webcamStream }}">
</div>
{% if enableTimelapse %}
<div class="tab-pane" id="timelapse">
<div style="display: none;" data-bind="visible: loginState.isUser">
<h1>Timelapse Configuration</h1>
{% if enableTimelapse %}
<h1>Timelapse Configuration</h1>
<label for="webcam_timelapse_mode">Timelapse Mode</label>
<select id="webcam_timelapse_mode" data-bind="value: timelapseType, enable: isOperational() && !isPrinting() && loginState.isUser()">
<option value="off">Off</option>
<option value="zchange">On Z Change</option>
<option value="timed">Timed</option>
</select>
<label for="webcam_timelapse_mode">Timelapse Mode</label>
<select id="webcam_timelapse_mode" data-bind="value: timelapseType, enable: isOperational() && !isPrinting()">
<option value="off">Off</option>
<option value="zchange">On Z Change</option>
<option value="timed">Timed</option>
</select>
<div id="webcam_timelapse_timedsettings" data-bind="visible: intervalInputEnabled()">
<label for="webcam_timelapse_interval">Interval</label>
<div class="input-append">
<input type="text" class="input-mini" id="webcam_timelapse_interval" data-bind="value: timelapseTimedInterval, enable: isOperational() && !isPrinting()">
<span class="add-on">sec</span>
<div id="webcam_timelapse_timedsettings" data-bind="visible: intervalInputEnabled()">
<label for="webcam_timelapse_interval">Interval</label>
<div class="input-append">
<input type="text" class="input-mini" id="webcam_timelapse_interval" data-bind="value: timelapseTimedInterval, enable: isOperational() && !isPrinting() && loginState.isUser()">
<span class="add-on">sec</span>
</div>
</div>
</div>
<div>
<button class="btn" data-bind="click: save, enable: isOperational() && !isPrinting()">Save Settings</button>
<div>
<button class="btn" data-bind="click: save, enable: isOperational() && !isPrinting() && loginState.isUser()">Save Settings</button>
</div>
</div>
<h1>Finished Timelapses</h1>
@ -491,18 +498,18 @@
</div>
<table class="table table-striped table-hover table-condensed table-hover" id="timelapse_files">
<thead>
<tr>
<th class="timelapse_files_name">Name</th>
<th class="timelapse_files_size">Size</th>
<th class="timelapse_files_action">Action</th>
</tr>
<tr>
<th class="timelapse_files_name">Name</th>
<th class="timelapse_files_size">Size</th>
<th class="timelapse_files_action">Action</th>
</tr>
</thead>
<tbody data-bind="foreach: listHelper.paginatedItems">
<tr data-bind="attr: {title: name}">
<td class="timelapse_files_name" data-bind="text: name"></td>
<td class="timelapse_files_size" data-bind="text: size"></td>
<td class="timelapse_files_action"><a href="#" class="icon-trash" data-bind="click: $parent.removeFile"></a>&nbsp;|&nbsp;<a href="#" class="icon-download" data-bind="attr: {href: url}"></a></td>
</tr>
<tr data-bind="attr: {title: name}">
<td class="timelapse_files_name" data-bind="text: name"></td>
<td class="timelapse_files_size" data-bind="text: size"></td>
<td class="timelapse_files_action"><a href="#" class="icon-trash" data-bind="click: function() { if ($root.loginState.isUser()) { $parent.removeFile(); } else { return; } }, css: {disabled: !$root.loginState.isUser()}"></a>&nbsp;|&nbsp;<a href="#" class="icon-download" data-bind="attr: {href: url}"></a></td>
</tr>
</tbody>
</table>
<div class="pagination pagination-mini pagination-centered">
@ -516,7 +523,6 @@
<li data-bind="css: {disabled: listHelper.currentPage() === listHelper.lastPage()}"><a href="#" data-bind="click: listHelper.nextPage">»</a></li>
</ul>
</div>
{% endif %}
</div>
{% endif %}
</div>
@ -532,24 +538,27 @@
</div>
</div>
{% include 'settings.html' %}
{% include 'dialogs.html' %}
{% include 'settings.jinja2' %}
{% include 'dialogs.jinja2' %}
<script type="text/javascript" src="http://code.jquery.com/jquery-latest.js"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/modernizr.custom.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/underscore-min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/knockout-2.2.1.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/bootstrap.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery.ui.core.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery.ui.widget.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery.ui.mouse.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery.ui.slider.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery.pnotify.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery.flot.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery.iframe-transport.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery.fileupload.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/socket.io.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/underscore.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/knockout.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/bootstrap/bootstrap.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/bootstrap/bootstrap-modalmanager.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/bootstrap/bootstrap-modal.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery/jquery.ui.core.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery/jquery.ui.widget.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery/jquery.ui.mouse.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery/jquery.ui.slider.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery/jquery.pnotify.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery/jquery.flot.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery/jquery.iframe-transport.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery/jquery.fileupload.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/socket.io/socket.io.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/ui.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='gcodeviewer/js/ui.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='gcodeviewer/js/gCodeReader.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='gcodeviewer/js/renderer.js') }}"></script>

View File

@ -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">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h3 id="settings_dialog_label">OctoPrint Settings</h3>
</div>
<div class="modal-body">
<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><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_folder" data-toggle="tab">Folder</a></li>
<li><a href="#settings_temperature" data-toggle="tab">Temperature</a></li>
<li><a href="#settings_appearance" data-toggle="tab">Appearance</a></li>
{% if enableAccessControl %}<li><a href="#settings_users" data-toggle="tab">Users</a></li>{% endif %}
</ul>
<div class="tab-content">
<div class="tab-content span8">
<div class="tab-pane active" id="settings_printerParameters">
<form class="form-horizontal">
<div class="control-group">
@ -198,6 +199,152 @@
</div>
</form>
</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>

View File

@ -83,7 +83,7 @@ class Timelapse(object):
with self._captureMutex:
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)
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 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):
# Make sure we are only handling one sending job at a time
with self._sendingLock:
cmd = cmd.upper()
if self._serial is None:
return
if 'M109' in cmd or 'M190' in cmd:

View File

@ -3,4 +3,6 @@ numpy>=1.6.2
pyserial>=2.6
tornado>=2.4.1
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
from octoprint.daemon import Daemon
from octoprint.server import Server