Merge branch 'devel' of https://github.com/foosel/OctoPrint into mrbeam

This commit is contained in:
Philipp Engel 2014-10-24 11:29:39 -05:00
commit 39bd44bf70
10 changed files with 183 additions and 27 deletions

View file

@ -30,12 +30,15 @@
* Better error reporting for timelapse rendering and system commands
* Custom control can now be defined so that they show a Confirm dialog with configurable text before executing
([#532](https://github.com/foosel/OctoPrint/issues/532) and [#590](https://github.com/foosel/OctoPrint/pull/590))
* Slicing has been greatly improved and now allows for a definition of slicing profiles to use for slicing plus overrides
which can be defined per slicing job. Slicers themselves are integrated into the system via ``SlicingPlugins``. Please
note that the [Cura integration](https://github.com/daid/Cura) has changed in such a way that OctoPrint now calls the
[CuraEngine](https://github.com/Ultimaker/CuraEngine) directly instead of depending on the full Cura installation. See
[the wiki](https://github.com/foosel/OctoPrint/wiki/Plugin:-Cura) for instructions on how to change your setup to
accommodate the new integration.
* Slicing has been greatly improved:
* It now allows for a definition of slicing profiles to use for slicing plus overrides which can be defined per slicing
job (defining overrides is not yet part of the UI but it's on the roadmap).
* Slicers themselves are integrated into the system via ``SlicingPlugins``.
* The [Cura integration](https://github.com/daid/Cura) has changed in such a way that OctoPrint now calls the
[CuraEngine](https://github.com/Ultimaker/CuraEngine) directly instead of depending on the full Cura installation. See
[the wiki](https://github.com/foosel/OctoPrint/wiki/Plugin:-Cura) for instructions on how to change your setup to
accommodate the new integration.
* The "Slicing done" notification is now colored green ([#558](https://github.com/foosel/OctoPrint/issues/558)).
* File management now supports STL files as first class citizens (including UI adjustments to allow management of
uploaded STL files including removal and reslicing) and also allows folders (not yet supported by UI)
@ -54,6 +57,7 @@
* The API is now enabled by default and the API key -- if not yet set -- will be automatically generated on first
server start and written back into ``config.yaml``
* Event subscriptions are now enabled by default (it was an accident that they weren't)
* Generate the key used for session hashing individually for each server instance
### Bug Fixes

View file

@ -1,11 +1,16 @@
Issues, Tickets, however you may call them
------------------------------------------
- If you want to report a bug, **READ [How to file a bug report](https://github.com/foosel/OctoPrint/wiki/How-to-file-a-bug-report)!** Tickets will be automatically checked if they comply with the requirements outlined in that wiki node! Other then what's written in there you don't have to do anything special with your ticket.
Read the following short instructions **fully** and **follow them** if you want your ticket to be taken care of and not closed again directly!
- Always create **one ticket for one purpose**. So don't mix two or more feature requests, support requests, bugs etc into one ticket. If you do, your ticket will be treated as if only describing the first purpose, the others will be ignored!
- If you want to report a bug, **READ AND FOLLOW [How to file a bug report](https://github.com/foosel/OctoPrint/wiki/How-to-file-a-bug-report)!** Tickets will be automatically checked if they comply with the requirements outlined in that wiki node! Other then what's written in there (**and really EVERYTHING that is written in there!**) you don't have to do anything special with your ticket.
- If you want to post a **request** of any kind (feature request, documentation request, ...), **add [Request] to your issue's title!**
- If you need **support** with a problem of your installation (e.g. if you have problems getting the webcam to work), **add [Support] to your issue's title!**
- If you have a general **question**, **add [Question] to your issue's title!**
- If you have another reason for creating a ticket that doesn't fit any of the above categories, **add [Misc] to your issue's title!**
- If you need **support** with a problem of your installation (e.g. if you have problems getting the webcam to work), **add [Support] to your issue's title!**. Note that for problems like these, the [Mailinglist](https://groups.google.com/group/octoprint) or the [Google+ Community](https://plus.google.com/communities/102771308349328485741) will probably get you help faster!
- If you have a general **question**, **add [Question] to your issue's title!**. Note that for problems like these, the [Mailinglist](https://groups.google.com/group/octoprint) or the [Google+ Community](https://plus.google.com/communities/102771308349328485741) will probably get you help faster!
- If you have another reason for creating a ticket that doesn't fit any of the above categories, think hard if it might not be something better suited for the [Mailinglist](https://groups.google.com/group/octoprint) or the [Google+ Community](https://plus.google.com/communities/102771308349328485741). If you are sure it needs to be reported here, **add [Misc] to your issue's title!**
Following these guidelines (**especially EVERYTHING mentioned in ["How to file a bug report"](https://github.com/foosel/OctoPrint/wiki/How-to-file-a-bug-report)**) is necessary so the tickets stay manageable - you are not the only one with an open issue, so please respect that you have to **play by the rules** so that your problem can be taken care of. Tickets not playing by the rules **will be closed without further investigation!**.
Pull Requests
-------------

View file

@ -89,6 +89,10 @@ def get_file_type(filename):
return get_path_for_extension(extension)
class NoSuchStorage(Exception):
pass
class FileManager(object):
def __init__(self, analysis_queue, slicing_manager, initial_storage_managers=None):
self._logger = logging.getLogger(__name__)
@ -293,7 +297,11 @@ class FileManager(object):
self._storage(destination).remove_link(path, rel, data)
def log_print(self, destination, path, timestamp, print_time, success):
self._storage(destination).add_history(path, dict(timestamp=timestamp, printTime=print_time, success=success))
try:
self._storage(destination).add_history(path, dict(timestamp=timestamp, printTime=print_time, success=success))
except NoSuchStorage:
# if there's no storage configured where to log the print, we'll just not log it
pass
def set_additional_metadata(self, destination, path, key, data, overwrite=False, merge=False):
self._storage(destination).set_additional_metadata(path, key, data, overwrite=overwrite, merge=merge)
@ -324,7 +332,7 @@ class FileManager(object):
def _storage(self, destination):
if not destination in self._storage_managers:
raise RuntimeError("No storage configured for destination {destination}".format(**locals()))
raise NoSuchStorage("No storage configured for destination {destination}".format(**locals()))
return self._storage_managers[destination]
def _on_analysis_finished(self, entry, result):

View file

@ -4,6 +4,7 @@ $(function() {
self.loginState = parameters[0];
self.settingsViewModel = parameters[1];
self.slicingViewModel = parameters[2];
self.fileName = ko.observable();
@ -101,6 +102,7 @@ $(function() {
$("#settings-plugin-cura-import").modal("hide");
self.requestData();
self.slicingViewModel.requestData();
}
});
@ -118,6 +120,7 @@ $(function() {
type: "DELETE",
success: function() {
self.requestData();
self.slicingViewModel.requestData();
}
});
};
@ -184,5 +187,5 @@ $(function() {
}
// view model class, parameters for constructor, container to bind to
ADDITIONAL_VIEWMODELS.push([CuraViewModel, ["loginStateViewModel", "settingsViewModel"], document.getElementById("settings_plugin_cura_dialog")]);
ADDITIONAL_VIEWMODELS.push([CuraViewModel, ["loginStateViewModel", "settingsViewModel", "slicingViewModel"], document.getElementById("settings_plugin_cura_dialog")]);
});

View file

@ -7,7 +7,7 @@ __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms
import uuid
from sockjs.tornado import SockJSRouter
from flask import Flask, render_template, send_from_directory, g, request, make_response
from flask import Flask, render_template, send_from_directory, g, request, make_response, session
from flask.ext.login import LoginManager
from flask.ext.principal import Principal, Permission, RoleNeed, identity_loaded, UserNeed
from flask.ext.babel import Babel
@ -160,8 +160,16 @@ def on_identity_loaded(sender, identity):
def load_user(id):
if session and "usersession.id" in session:
sessionid = session["usersession.id"]
else:
sessionid = None
if userManager is not None:
return userManager.findUser(id)
if sessionid:
return userManager.findUser(username=id, session=sessionid)
else:
return userManager.findUser(username=id)
return users.DummyUser()
@ -256,7 +264,15 @@ class Server():
settings().get(["server", "reverseProxy", "prefixScheme"])
)
app.secret_key = "k3PuVYgtxNm8DXKKTw2nWmFQQun9qceV"
secret_key = settings().get(["server", "secretKey"])
if not secret_key:
import string
from random import choice
chars = string.ascii_lowercase + string.ascii_uppercase + string.digits
secret_key = "".join(choice(chars) for _ in xrange(32))
settings().set(["server", "secretKey"], secret_key)
settings().save()
app.secret_key = secret_key
loginManager = LoginManager()
loginManager.session_protection = "strong"
loginManager.user_callback = load_user

View file

@ -244,15 +244,26 @@ def login():
else:
remember = False
if "usersession.id" in session:
_logout(current_user)
user = octoprint.server.userManager.findUser(username)
if user is not None:
if user.check_password(octoprint.users.UserManager.createPasswordHash(password)):
if octoprint.server.userManager is not None:
user = octoprint.server.userManager.login_user(user)
session["usersession.id"] = user.get_session()
login_user(user, remember=remember)
identity_changed.send(current_app._get_current_object(), identity=Identity(user.get_id()))
return jsonify(user.asDict())
return make_response(("User unknown or password incorrect", 401, []))
elif "passive" in request.values.keys():
user = current_user
if octoprint.server.userManager is not None:
user = octoprint.server.userManager.login_user(current_user)
else:
user = current_user
if user is not None and not user.is_anonymous():
identity_changed.send(current_app._get_current_object(), identity=Identity(user.get_id()))
return jsonify(user.asDict())
@ -288,7 +299,12 @@ def logout():
del session[key]
identity_changed.send(current_app._get_current_object(), identity=AnonymousIdentity())
_logout(current_user)
logout_user()
return NO_CONTENT
def _logout(user):
if "usersession.id" in session:
del session["usersession.id"]
octoprint.server.userManager.logout_user(user)

View file

@ -41,6 +41,7 @@ default_settings = {
"host": "0.0.0.0",
"port": 5000,
"firstRun": True,
"secretKey": None,
"reverseProxy": {
"prefixHeader": "X-Script-Name",
"schemeHeader": "X-Scheme",

View file

@ -155,7 +155,7 @@ function DataUpdater(allViewModels) {
var payload = data["payload"];
var html = "";
console.log("Got event " + type + " with payload: " + JSON.stringify(payload))
console.log("Got event " + type + " with payload: " + JSON.stringify(payload));
if (type == "UpdatedFiles") {
_.each(self.allViewModels, function(viewModel) {
@ -186,7 +186,7 @@ function DataUpdater(allViewModels) {
gcodeUploadProgress.removeClass("progress-striped").removeClass("active");
gcodeUploadProgressBar.css("width", "0%");
gcodeUploadProgressBar.text("");
new PNotify({title: gettext("Slicing done"), text: _.sprintf(gettext("Sliced %(stl)s to %(gcode)s, took %(time).2f seconds"), payload)});
new PNotify({title: gettext("Slicing done"), text: _.sprintf(gettext("Sliced %(stl)s to %(gcode)s, took %(time).2f seconds"), payload), type: "success"});
_.each(self.allViewModels, function (viewModel) {
if (viewModel.hasOwnProperty("onSlicingDone")) {
@ -222,7 +222,7 @@ function DataUpdater(allViewModels) {
gcodeUploadProgress.removeClass("progress-striped").removeClass("active");
gcodeUploadProgressBar.css("width", "0%");
gcodeUploadProgressBar.text("");
new PNotify({title: gettext("Streaming done"), text: _.sprintf(gettext("Streamed %(local)s to %(remote)s on SD, took %(time).2f seconds"), payload)});
new PNotify({title: gettext("Streaming done"), text: _.sprintf(gettext("Streamed %(local)s to %(remote)s on SD, took %(time).2f seconds"), payload), type: "success"});
gcodeFilesViewModel.requestData(payload.remote, "sdcard");
}
break;

View file

@ -7,6 +7,9 @@ function SlicingViewModel(loginStateViewModel) {
self.file = undefined;
self.data = undefined;
self.defaultSlicer = undefined;
self.defaultProfile = undefined;
self.gcodeFilename = ko.observable();
self.title = ko.observable();
@ -67,6 +70,8 @@ function SlicingViewModel(loginStateViewModel) {
if (selectedSlicer != undefined) {
self.slicer(selectedSlicer);
}
self.defaultSlicer = selectedSlicer;
};
self.profilesForSlicer = function(key) {
@ -99,6 +104,8 @@ function SlicingViewModel(loginStateViewModel) {
if (selectedProfile != undefined) {
self.profile(selectedProfile);
}
self.defaultProfile = selectedProfile;
};
self.slice = function() {
@ -127,8 +134,8 @@ function SlicingViewModel(loginStateViewModel) {
$("#slicing_configuration_dialog").modal("hide");
self.gcodeFilename(undefined);
self.slicer(undefined);
self.profile(undefined);
self.slicer(self.defaultSlicer);
self.profile(self.defaultProfile);
};
self._sanitize = function(name) {

View file

@ -1,6 +1,9 @@
# coding=utf-8
from __future__ import absolute_import
__author__ = "Gina Häußge <osd@foosel.net>"
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
__copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms of the AGPLv3 License"
from flask.ext.login import UserMixin
from flask.ext.principal import Identity
@ -9,11 +12,63 @@ import os
import yaml
import uuid
import logging
from octoprint.settings import settings
class UserManager(object):
valid_roles = ["user", "admin"]
def __init__(self):
self._logger = logging.getLogger(__name__)
self._session_users_by_session = dict()
self._session_users_by_username = dict()
def login_user(self, user):
self._cleanup_sessions()
if user is None:
return None
if not isinstance(user, SessionUser):
user = SessionUser(user)
self._session_users_by_session[user.get_session()] = user
if not user.get_name() in self._session_users_by_username:
self._session_users_by_username[user.get_name()] = []
self._session_users_by_username[user.get_name()].append(user)
self._logger.debug("Logged in user: %r" % user)
return user
def logout_user(self, user):
if user is None:
return
if not isinstance(user, SessionUser):
return
if user.get_name() in self._session_users_by_username:
users_by_username = self._session_users_by_username[user.get_name()]
for u in users_by_username:
if u.get_session() == user.get_session():
users_by_username.remove(u)
break
if user.get_session() in self._session_users_by_session:
del self._session_users_by_session[user.get_session()]
self._logger.debug("Logged out user: %r" % user)
def _cleanup_sessions(self):
import time
for session, user in self._session_users_by_session.items():
if not isinstance(user, SessionUser):
continue
if user._created + (24 * 60 * 60) < time.time():
self.logout_user(user)
@staticmethod
def createPasswordHash(password):
return hashlib.sha512(password + "mvBUTvwzBzD3yPwvnJ4E4tXNf3CGJvvW").hexdigest()
@ -37,9 +92,22 @@ class UserManager(object):
pass
def removeUser(self, username):
pass
if username in self._session_users_by_username:
users = self._session_users_by_username[username]
sessions = [user.get_session() for user in users if isinstance(user, SessionUser)]
for session in sessions:
if session in self._session_users_by_session:
del self._session_users_by_session[session]
del self._session_users_by_username[username]
def findUser(self, username=None, session=None):
if session is not None:
for session in self._session_users_by_session:
user = self._session_users_by_session[session]
if username is None or username == user.get_name():
return user
break
def findUser(self, username=None):
return None
def getAllUsers(self):
@ -179,6 +247,8 @@ class FilebasedUserManager(UserManager):
self._save()
def removeUser(self, username):
UserManager.removeUser(self, username)
if not username in self._users.keys():
raise UnknownUser(username)
@ -186,17 +256,23 @@ class FilebasedUserManager(UserManager):
self._dirty = True
self._save()
def findUser(self, username=None, apikey=None):
def findUser(self, username=None, apikey=None, session=None):
user = UserManager.findUser(self, username=username, session=session)
if user is not None:
return user
if username is not None:
if username not in self._users.keys():
return None
return self._users[username]
elif apikey is not None:
for user in self._users.values():
if apikey == user._apikey:
return user
return None
else:
return None
@ -257,6 +333,26 @@ class User(UserMixin):
def is_admin(self):
return "admin" in self._roles
def __repr__(self):
return "User(id=%s,name=%s,active=%r,user=%r,admin=%r)" % (self.get_id(), self.get_name(), self.is_active(), self.is_user(), self.is_admin())
class SessionUser(User):
def __init__(self, user):
User.__init__(self, user._username, user._passwordHash, user._active, user._roles, user._apikey)
import string
import random
import time
chars = string.ascii_uppercase + string.ascii_lowercase + string.digits
self._session = "".join(random.choice(chars) for _ in xrange(10))
self._created = time.time()
def get_session(self):
return self._session
def __repr__(self):
return "SessionUser(id=%s,name=%s,active=%r,user=%r,admin=%r,session=%s,created=%s)" % (self.get_id(), self.get_name(), self.is_active(), self.is_user(), self.is_admin(), self._session, self._created)
##~~ DummyUser object to use when accessControl is disabled
class DummyUser(User):
@ -274,7 +370,7 @@ def dummy_identity_loader():
return DummyIdentity()
##~~ Apiuser object to use when api key is used to access the API
##~~ Apiuser object to use when global api key is used to access the API
class ApiUser(User):