commit
d721e5edc2
|
@ -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,21 +112,25 @@ 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():
|
||||
if "command" in request.values.keys() and request.values["command"] == "connect":
|
||||
port = None
|
||||
baudrate = None
|
||||
if "port" in request.values.keys():
|
||||
|
@ -125,14 +142,13 @@ def connect():
|
|||
settings().setInt(["serial", "baudrate"], baudrate)
|
||||
settings().save()
|
||||
printer.connect(port=port, baudrate=baudrate)
|
||||
return jsonify(state="Connecting")
|
||||
|
||||
@app.route(BASEURL + "control/disconnect", methods=["POST"])
|
||||
def disconnect():
|
||||
elif "command" in request.values.keys() and request.values["command"] == "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():
|
||||
@app.route(BASEURL + "control/job", methods=["POST"])
|
||||
@login_required
|
||||
def printJobControl():
|
||||
if "command" in request.values.keys():
|
||||
if request.values["command"] == "start":
|
||||
printer.startPrint()
|
||||
return jsonify(SUCCESS)
|
||||
|
||||
@app.route(BASEURL + "control/pause", methods=["POST"])
|
||||
def pausePrint():
|
||||
elif request.values["command"] == "pause":
|
||||
printer.togglePausePrint()
|
||||
return jsonify(SUCCESS)
|
||||
|
||||
@app.route(BASEURL + "control/cancel", methods=["POST"])
|
||||
def cancelPrint():
|
||||
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"):
|
||||
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()
|
||||
|
|
|
@ -72,6 +72,11 @@ default_settings = {
|
|||
"controls": [],
|
||||
"system": {
|
||||
"actions": []
|
||||
},
|
||||
"accessControl": {
|
||||
"enabled": False,
|
||||
"userManager": "octoprint.users.FilebasedUserManager",
|
||||
"userfile": None
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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 */
|
||||
|
|
|
@ -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"});
|
||||
}
|
||||
|
|
|
@ -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);
|
|
@ -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);
|
|
@ -18,7 +18,7 @@
|
|||
// Register as an anonymous AMD module:
|
||||
define([
|
||||
'jquery',
|
||||
'jquery.ui.widget'
|
||||
'jquery.ui.widget.js'
|
||||
], factory);
|
||||
} else {
|
||||
// Browser globals:
|
|
@ -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");
|
||||
}
|
||||
|
||||
function TemperatureViewModel(settingsViewModel) {
|
||||
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(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,32 +1728,34 @@ 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" }
|
||||
|
@ -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) {
|
||||
gcodeViewModel.initialize();
|
||||
}
|
||||
|
||||
//~~ startup commands
|
||||
|
||||
loginStateViewModel.requestData();
|
||||
connectionViewModel.requestData();
|
||||
controlsViewModel.requestData();
|
||||
controlViewModel.requestData();
|
||||
gcodeFilesViewModel.requestData();
|
||||
webcamViewModel.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();
|
||||
});
|
||||
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -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> | <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> | <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> | <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> | <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,10 +175,11 @@
|
|||
<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">
|
||||
<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">
|
||||
<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>
|
||||
|
@ -160,15 +191,15 @@
|
|||
</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,14 +215,15 @@
|
|||
|
||||
<label>Target: <strong data-bind="html: targetTempString"></strong></label>
|
||||
|
||||
<div style="display: none;" data-bind="visible: loginState.isUser">
|
||||
<label for="temp_newTemp">New Target</label>
|
||||
<div class="input-append">
|
||||
<input type="text" id="temp_newTemp" data-bind="attr: {placeholder: targetTemp}" class="tempInput">
|
||||
<input type="text" id="temp_newTemp" data-bind="attr: {placeholder: targetTemp}, enable: isOperational() && loginState.isUser()" class="tempInput">
|
||||
<span class="add-on">°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">
|
||||
<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">
|
||||
|
@ -207,6 +239,7 @@
|
|||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-horizontal span6">
|
||||
<h1>Bed Temperature</h1>
|
||||
|
||||
|
@ -214,14 +247,15 @@
|
|||
|
||||
<label>Target: <strong data-bind="html: bedTargetTempString"></strong></label>
|
||||
|
||||
<div style="display: none;" data-bind="visible: loginState.isUser">
|
||||
<label for="temp_newBedTemp">New Target</label>
|
||||
<div class="input-append">
|
||||
<input type="text" id="temp_newBedTemp" data-bind="attr: {placeholder: bedTargetTemp}" class="tempInput">
|
||||
<input type="text" id="temp_newBedTemp" data-bind="attr: {placeholder: bedTargetTemp}, enable: isOperational() && loginState.isUser()" class="tempInput">
|
||||
<span class="add-on">°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">
|
||||
<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">
|
||||
|
@ -239,70 +273,77 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane" id="controls">
|
||||
<div class="jog-panel">
|
||||
</div>
|
||||
<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> 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> 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,22 +461,18 @@
|
|||
<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>
|
||||
|
||||
<label for="webcam_timelapse_mode">Timelapse Mode</label>
|
||||
<select id="webcam_timelapse_mode" data-bind="value: timelapseType, enable: isOperational() && !isPrinting()">
|
||||
<select id="webcam_timelapse_mode" data-bind="value: timelapseType, enable: isOperational() && !isPrinting() && loginState.isUser()">
|
||||
<option value="off">Off</option>
|
||||
<option value="zchange">On Z Change</option>
|
||||
<option value="timed">Timed</option>
|
||||
|
@ -475,13 +481,14 @@
|
|||
<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()">
|
||||
<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>
|
||||
<button class="btn" data-bind="click: save, enable: isOperational() && !isPrinting()">Save Settings</button>
|
||||
<button class="btn" data-bind="click: save, enable: isOperational() && !isPrinting() && loginState.isUser()">Save Settings</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1>Finished Timelapses</h1>
|
||||
|
@ -501,7 +508,7 @@
|
|||
<tr data-bind="attr: {title: name}">
|
||||
<td class="timelapse_files_name" data-bind="text: name"></td>
|
||||
<td class="timelapse_files_size" data-bind="text: size"></td>
|
||||
<td class="timelapse_files_action"><a href="#" class="icon-trash" data-bind="click: $parent.removeFile"></a> | <a href="#" class="icon-download" data-bind="attr: {href: url}"></a></td>
|
||||
<td class="timelapse_files_action"><a href="#" class="icon-trash" data-bind="click: function() { if ($root.loginState.isUser()) { $parent.removeFile(); } else { return; } }, css: {disabled: !$root.loginState.isUser()}"></a> | <a href="#" class="icon-download" data-bind="attr: {href: url}"></a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -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>
|
|
@ -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">×</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> | <a href="#" class="icon-key" title="Change password" data-bind="click: function() { $root.users.showChangePasswordDialog($data); }"></a> | <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">×</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">×</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">×</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>
|
|
@ -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})
|
||||
|
|
|
@ -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()
|
|
@ -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
|
|
@ -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:
|
||||
|
|
|
@ -4,3 +4,5 @@ pyserial>=2.6
|
|||
tornado>=2.4.1
|
||||
tornadio2>=0.0.4
|
||||
PyYAML>=3.10
|
||||
Flask-Login>=0.1.3
|
||||
Flask-Principal>=0.3.5
|
Loading…
Reference in New Issue