Merge branch 'master' into devel

This commit is contained in:
Gina Häußge 2015-06-19 19:21:44 +02:00
commit 65ae48d992
15 changed files with 491 additions and 174 deletions

View file

@ -136,8 +136,16 @@ class FileManager(object):
def initialize(self):
self.reload_plugins()
for storage_type, storage_manager in self._storage_managers.items():
self._determine_analysis_backlog(storage_type, storage_manager)
def worker():
self._logger.info("Adding backlog items from all storage types to analysis queue...".format(**locals()))
for storage_type, storage_manager in self._storage_managers.items():
self._determine_analysis_backlog(storage_type, storage_manager)
import threading
thread = threading.Thread(target=worker)
thread.daemon = True
thread.start()
def reload_plugins(self):
self._progress_plugins = octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.ProgressPlugin)
@ -150,13 +158,15 @@ class FileManager(object):
self._slicing_progress_callbacks.remove(callback)
def _determine_analysis_backlog(self, storage_type, storage_manager):
self._logger.info("Adding backlog items from {storage_type} to analysis queue".format(**locals()))
counter = 0
for entry, path, printer_profile in storage_manager.analysis_backlog:
file_type = get_file_type(path)[-1]
# we'll use the default printer profile for the backlog since we don't know better
queue_entry = QueueEntry(entry, file_type, storage_type, path, self._printer_profile_manager.get_default())
self._analysis_queue.enqueue(queue_entry, high_priority=False)
counter += 1
self._logger.info("Added {counter} items from storage type \"{storage_type}\" to analysis queue".format(**locals()))
def add_storage(self, storage_type, storage_manager):
self._storage_managers[storage_type] = storage_manager

View file

@ -309,6 +309,41 @@ class LocalFileStorage(StorageInterface):
self._metadata_cache = pylru.lrucache(10)
self._old_metadata = None
self._initialize_metadata()
def _initialize_metadata(self):
self._logger.info("Initializing the file metadata for {}...".format(self.basefolder))
old_metadata_path = os.path.join(self.basefolder, "metadata.yaml")
backup_path = os.path.join(self.basefolder, "metadata.yaml.backup")
if os.path.exists(old_metadata_path):
# load the old metadata file
try:
with open(old_metadata_path) as f:
import yaml
self._old_metadata = yaml.safe_load(f)
except:
self._logger.exception("Error while loading old metadata file")
# make sure the metadata is initialized as far as possible
self._list_folder(self.basefolder)
# rename the old metadata file
self._old_metadata = None
try:
import shutil
shutil.move(old_metadata_path, backup_path)
except:
self._logger.exception("Could not rename old metadata.yaml file")
else:
# make sure the metadata is initialized as far as possible
self._list_folder(self.basefolder)
self._logger.info("... file metadata for {} initialized successfully.".format(self.basefolder))
@property
def analysis_backlog(self):
for entry in self._analysis_backlog_generator():
@ -919,12 +954,7 @@ class LocalFileStorage(StorageInterface):
if entry in metadata and isinstance(metadata[entry], dict):
entry_data = metadata[entry]
else:
entry_data = dict(
hash=self._create_hash(entry_path),
links=[],
notes=[]
)
metadata[entry] = entry_data
entry_data = self._add_basic_metadata(path, entry, save=False, metadata=metadata)
metadata_dirty = True
# TODO extract model hash from source if possible to recreate link
@ -959,6 +989,31 @@ class LocalFileStorage(StorageInterface):
return result
def _add_basic_metadata(self, path, entry, additional_metadata=None, save=True, metadata=None):
if additional_metadata is None:
additional_metadata = dict()
if metadata is None:
metadata = self._get_metadata(path)
entry_data = dict(
hash=self._create_hash(os.path.join(path, entry)),
links=[],
notes=[]
)
if path == self.basefolder and self._old_metadata is not None and entry in self._old_metadata and "gcodeAnalysis" in self._old_metadata[entry]:
# if there is still old metadata available and that contains an analysis for this file, use it!
entry_data["analysis"] = self._old_metadata[entry]["gcodeAnalysis"]
entry_data.update(additional_metadata)
metadata[entry] = entry_data
if save:
self._save_metadata(path, metadata)
return entry_data
def _create_hash(self, path):
import hashlib
@ -993,16 +1048,22 @@ class LocalFileStorage(StorageInterface):
def _save_metadata(self, path, metadata):
metadata_path = os.path.join(path, ".metadata.yaml")
fh, metadata_temporary_path = tempfile.mkstemp()
os.close(fh)
with self._metadata_lock:
try:
with open(metadata_temporary_path, "w") as f:
import yaml
yaml.safe_dump(metadata, stream=f, default_flow_style=False, indent=" ", allow_unicode=True)
import yaml
import shutil
shutil.move(metadata_temporary_path, metadata_path)
file_obj = tempfile.NamedTemporaryFile(delete=False)
try:
yaml.safe_dump(metadata, stream=file_obj, default_flow_style=False, indent=" ", allow_unicode=True)
file_obj.close()
shutil.move(file_obj.name, metadata_path)
finally:
try:
if os.path.exists(file_obj.name):
os.remove(file_obj.name)
except Exception as e:
self._logger.warn("Could not delete file {}: {}".format(file_obj.name, str(e)))
except:
self._logger.exception("Error while writing .metadata.yaml to {path}".format(**locals()))
else:

View file

@ -281,6 +281,7 @@ class PluginSettings(object):
defaults = dict()
self.defaults = dict(plugins=dict())
self.defaults["plugins"][plugin_key] = defaults
self.defaults["plugins"][plugin_key]["_config_version"] = None
if get_preprocessors is None:
get_preprocessors = dict()
@ -309,11 +310,17 @@ class PluginSettings(object):
return result
def add_getter_kwargs(kwargs):
kwargs.update(defaults=self.defaults, preprocessors=self.get_preprocessors)
if not "defaults" in kwargs:
kwargs.update(defaults=self.defaults)
if not "preprocessors" in kwargs:
kwargs.update(preprocessors=self.get_preprocessors)
return kwargs
def add_setter_kwargs(kwargs):
kwargs.update(defaults=self.defaults, preprocessors=self.set_preprocessors)
if not "defaults" in kwargs:
kwargs.update(defaults=self.defaults)
if not "preprocessors" in kwargs:
kwargs.update(preprocessors=self.set_preprocessors)
return kwargs
self.access_methods = dict(

View file

@ -428,7 +428,9 @@ class PluginManager(object):
It is able to discover plugins both through possible file system locations as well as customizable entry points.
"""
def __init__(self, plugin_folders, plugin_types, plugin_entry_points, logging_prefix=None, plugin_disabled_list=None, plugin_restart_needing_hooks=None, plugin_obsolete_hooks=None, plugin_validators=None):
def __init__(self, plugin_folders, plugin_types, plugin_entry_points, logging_prefix=None,
plugin_disabled_list=None, plugin_restart_needing_hooks=None, plugin_obsolete_hooks=None,
plugin_validators=None):
self.logger = logging.getLogger(__name__)
if logging_prefix is None:
@ -453,6 +455,8 @@ class PluginManager(object):
self.implementation_injects = dict()
self.implementation_inject_factories = []
self.implementation_pre_inits = []
self.implementation_post_inits = []
self.on_plugin_loaded = lambda *args, **kwargs: None
self.on_plugin_unloaded = lambda *args, **kwargs: None
@ -849,27 +853,35 @@ class PluginManager(object):
return False
return hook in self.plugin_obsolete_hooks
def initialize_implementations(self, additional_injects=None, additional_inject_factories=None):
def initialize_implementations(self, additional_injects=None, additional_inject_factories=None, additional_pre_inits=None, additional_post_inits=None):
for name, plugin in self.enabled_plugins.items():
self.initialize_implementation_of_plugin(name, plugin,
additional_injects=additional_injects,
additional_inject_factories=additional_inject_factories)
additional_inject_factories=additional_inject_factories,
additional_pre_inits=additional_pre_inits,
additional_post_inits=additional_post_inits)
self.logger.info("Initialized {count} plugin(s)".format(count=len(self.plugin_implementations)))
def initialize_implementation_of_plugin(self, name, plugin, additional_injects=None, additional_inject_factories=None):
def initialize_implementation_of_plugin(self, name, plugin, additional_injects=None, additional_inject_factories=None, additional_pre_inits=None, additional_post_inits=None):
if plugin.implementation is None:
return
return self.initialize_implementation(name, plugin, plugin.implementation,
additional_injects=additional_injects,
additional_inject_factories=additional_inject_factories)
additional_inject_factories=additional_inject_factories,
additional_pre_inits=additional_pre_inits,
additional_post_inits=additional_post_inits)
def initialize_implementation(self, name, plugin, implementation, additional_injects=None, additional_inject_factories=None):
def initialize_implementation(self, name, plugin, implementation, additional_injects=None, additional_inject_factories=None, additional_pre_inits=None, additional_post_inits=None):
if additional_injects is None:
additional_injects = dict()
if additional_inject_factories is None:
additional_inject_factories = []
if additional_pre_inits is None:
additional_pre_inits = []
if additional_post_inits is None:
additional_post_inits = []
injects = self.implementation_injects
injects.update(additional_injects)
@ -877,6 +889,12 @@ class PluginManager(object):
inject_factories = self.implementation_inject_factories
inject_factories += additional_inject_factories
pre_inits = self.implementation_pre_inits
pre_inits += additional_pre_inits
post_inits = self.implementation_post_inits
post_inits += additional_post_inits
try:
kwargs = dict(injects)
@ -904,8 +922,16 @@ class PluginManager(object):
for arg, value in return_value.items():
setattr(implementation, "_" + arg, value)
# execute any additional pre init methods
for pre_init in pre_inits:
pre_init(name, implementation)
implementation.initialize()
# execute any additional post init methods
for post_init in post_inits:
post_init(name, implementation)
except Exception as e:
self._deactivate_plugin(name, plugin)
plugin.enabled = False
@ -1189,13 +1215,13 @@ class RestartNeedingPlugin(Plugin):
class PluginNeedsRestart(BaseException):
def __init__(self, name):
super(BaseException, self).__init__()
BaseException.__init__(self)
self.name = name
self.message = "Plugin {name} cannot be enabled or disabled after system startup".format(**locals())
class PluginLifecycleException(BaseException):
def __init__(self, name, reason, message):
super(BaseException, self).__init__()
BaseException.__init__(self)
self.name = name
self.reason = reason
@ -1206,7 +1232,7 @@ class PluginLifecycleException(BaseException):
class PluginCantInitialize(PluginLifecycleException):
def __init__(self, name, reason):
super(PluginLifecycleException, self).__init__(name, reason, "Plugin {name} cannot be initialized: {reason}")
PluginLifecycleException.__init__(self, name, reason, "Plugin {name} cannot be initialized: {reason}")
class PluginCantEnable(PluginLifecycleException):
def __init__(self, name, reason):
@ -1214,4 +1240,4 @@ class PluginCantEnable(PluginLifecycleException):
class PluginCantDisable(PluginLifecycleException):
def __init__(self, name, reason):
super(PluginLifecycleException, self).__init__(name, reason, "Plugin {name} cannot be disabled: {reason}")
PluginLifecycleException.__init__(self, name, reason, "Plugin {name} cannot be disabled: {reason}")

View file

@ -718,7 +718,7 @@ class SettingsPlugin(OctoPrintPlugin):
def on_settings_save(self, data):
old_flag = self._settings.get_boolean(["sub", "some_flag"])
super(MySettingsPlugin, self).on_settings_save(data)
octoprint.plugin.SettingsPlugin.on_settings_save(self, data)
new_flag = self._settings.get_boolean(["sub", "some_flag"])
if old_flag != new_flag:
@ -830,6 +830,41 @@ class SettingsPlugin(OctoPrintPlugin):
"""
return dict(), dict()
def get_settings_version(self):
"""
Retrieves the settings format version of the plugin.
Use this to have OctoPrint trigger your migration function if it detects an outdated settings version in
config.yaml.
Returns:
int or None: an int signifying the current settings format, should be incremented by plugins whenever there
are backwards incompatible changes. Returning None here disables the version tracking for the
plugin's configuration.
"""
return None
def on_settings_migrate(self, target, current):
"""
Called by OctoPrint if it detects that the installed version of the plugin necessitates a higher settings version
than the one currently stored in _config.yaml. Will also be called if the settings data stored in config.yaml
doesn't have version information, in which case the ``current`` parameter will be None.
Your plugin's implementation should take care of migrating any data by utilizing self._settings. OctoPrint
will take care of saving any changes to disk by calling `self._settings.save()` after returning from this method.
This method will be called before your plugin's :func:`initialize` method, but with all injections already
having taken place. You can therefore depend on the configuration having been migrated by the time :func:`initialize`
is called.
Arguments:
target (int): The settings format version the plugin requires, this should always be the same value as
returned by :func:`get_settings_version`.
current (int or None): The settings format version as currently stored in config.yaml. May be None if
no version information can be found.
"""
pass
class EventHandlerPlugin(OctoPrintPlugin):
"""

View file

@ -129,7 +129,7 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin,
def on_settings_save(self, data):
old_debug_logging = self._settings.get_boolean(["debug_logging"])
super(CuraPlugin, self).on_settings_save(data)
octoprint.plugin.SettingsPlugin.on_settings_save(self, data)
new_debug_logging = self._settings.get_boolean(["debug_logging"])
if old_debug_logging != new_debug_logging:
@ -410,4 +410,4 @@ __plugin_author__ = "Gina Häußge"
__plugin_url__ = "https://github.com/foosel/OctoPrint/wiki/Plugin:-Cura"
__plugin_description__ = "Adds support for slicing via CuraEngine from within OctoPrint"
__plugin_license__ = "AGPLv3"
__plugin_implementation__ = CuraPlugin()
__plugin_implementation__ = CuraPlugin()

View file

@ -133,7 +133,7 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
result.append(self._to_external_representation(plugin))
if "refresh_repository" in request.values and request.values["refresh_repository"] in valid_boolean_trues:
self._refresh_repository()
self._repository_available = self._refresh_repository()
return jsonify(plugins=result, repository=dict(available=self._repository_available, plugins=self._repository_plugins), os=self._get_os(), octoprint=self._get_octoprint_version())
@ -141,6 +141,10 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
if not admin_permission.can():
return make_response("Insufficient rights", 403)
if self._printer.is_printing() or self._printer.is_paused():
# do not update while a print job is running
return make_response("Printer is currently printing or paused", 409)
if command == "install":
url = data["url"]
plugin_name = data["plugin"] if "plugin" in data else None
@ -165,10 +169,6 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
plugin = self._plugin_manager.plugins[plugin_name]
return self.command_toggle(plugin, command)
elif command == "refresh_repository":
self._repository_available = self._refresh_repository()
return jsonify(repository=dict(available=self._repository_available, plugins=self._repository_plugins))
def command_install(self, url=None, path=None, force=False, reinstall=None, dependency_links=False):
if url is not None:
pip_args = ["install", sarge.shell_quote(url)]

View file

@ -4,6 +4,7 @@ $(function() {
self.loginState = parameters[0];
self.settingsViewModel = parameters[1];
self.printerState = parameters[2];
self.plugins = new ItemListHelper(
"plugin.pluginmanager.installedplugins",
@ -76,6 +77,22 @@ $(function() {
self.workingDialog = undefined;
self.workingOutput = undefined;
self.enableManagement = ko.computed(function() {
return !self.printerState.isPrinting();
});
self.enableToggle = function(data) {
return self.enableManagement() && data.key != 'pluginmanager';
};
self.enableUninstall = function(data) {
return self.enableManagement() && !data.bundled && data.key != 'pluginmanager' && !data.pending_uninstall;
};
self.enableRepoInstall = function(data) {
return self.enableManagement() && self.isCompatible(data);
};
self.invalidUrl = ko.computed(function() {
var url = self.installUrl();
return url !== undefined && url.trim() != "" && !(_.startsWith(url.toLocaleLowerCase(), "http://") || _.startsWith(url.toLocaleLowerCase(), "https://"));
@ -83,7 +100,7 @@ $(function() {
self.enableUrlInstall = ko.computed(function() {
var url = self.installUrl();
return url !== undefined && url.trim() != "" && !self.invalidUrl();
return self.enableManagement() && url !== undefined && url.trim() != "" && !self.invalidUrl();
});
self.invalidArchive = ko.computed(function() {
@ -93,7 +110,7 @@ $(function() {
self.enableArchiveInstall = ko.computed(function() {
var name = self.uploadFilename();
return name !== undefined && name.trim() != "" && !self.invalidArchive();
return self.enableManagement() && name !== undefined && name.trim() != "" && !self.invalidArchive();
});
self.uploadElement.fileupload({
@ -187,6 +204,10 @@ $(function() {
return;
}
if (!self.enableManagement()) {
return;
}
if (data.key == "pluginmanager") return;
var command = self._getToggleCommand(data);
@ -217,6 +238,10 @@ $(function() {
return;
}
if (!self.enableManagement()) {
return;
}
if (self.installed(data)) {
self.installPlugin(data.archive, data.title, data.id, data.follow_dependency_links || self.followDependencyLinks());
} else {
@ -229,6 +254,10 @@ $(function() {
return;
}
if (!self.enableManagement()) {
return;
}
if (url === undefined) {
url = self.installUrl();
}
@ -279,6 +308,10 @@ $(function() {
return;
}
if (!self.enableManagement()) {
return;
}
if (data.bundled) return;
if (data.key == "pluginmanager") return;
@ -305,9 +338,7 @@ $(function() {
return;
}
self._postCommand("refresh_repository", {}, function(data) {
self._fromRepositoryResponse(data.repository);
})
self.requestData(true);
};
self.installed = function(data) {
@ -420,7 +451,7 @@ $(function() {
self.toggleButtonCss = function(data) {
var icon = self._getToggleCommand(data) == "enable" ? "icon-circle-blank" : "icon-circle";
var disabled = (data.key == "pluginmanager") ? " disabled" : "";
var disabled = (self.enableToggle(data)) ? "" : " disabled";
return icon + disabled;
};
@ -578,5 +609,5 @@ $(function() {
}
// view model class, parameters for constructor, container to bind to
ADDITIONAL_VIEWMODELS.push([PluginManagerViewModel, ["loginStateViewModel", "settingsViewModel"], "#settings_plugin_pluginmanager"]);
ADDITIONAL_VIEWMODELS.push([PluginManagerViewModel, ["loginStateViewModel", "settingsViewModel", "printerStateViewModel"], "#settings_plugin_pluginmanager"]);
});

View file

@ -1,3 +1,11 @@
{% macro pluginmanager_printing() %}
<div class="alert" data-bind="visible: !enableManagement()">
{{ _('Take note that all plugin management functionality is disabled while your printer is printing.') }}
</div>
{% endmacro %}
{{ pluginmanager_printing() }}
<h3>{{ _('Installed Plugins') }}</h3>
<table class="table table-striped table-hover table-condensed table-hover">
@ -20,7 +28,7 @@
</div>
</td>
<td class="settings_plugin_plugin_manager_plugins_actions">
<a href="#" data-bind="css: $root.toggleButtonCss($data), attr: {title: $root.toggleButtonTitle($data)}, enable: key != 'pluginmanager', click: function() { $root.togglePlugin($data) }"></a>&nbsp;|&nbsp;<a href="#" class="icon-trash" title="{{ _('Uninstall Plugin') }}" data-bind="css: {disabled: bundled || key == 'pluginmanager' || pending_uninstall}, enable: !bundled && key != 'pluginmanager' && !pending_uninstall, click: function() { $root.uninstallPlugin($data) }"></a>
<a href="#" data-bind="css: $root.toggleButtonCss($data), attr: {title: $root.toggleButtonTitle($data)}, enable: $root.enableToggle($data), click: function() { $root.togglePlugin($data) }"></a>&nbsp;|&nbsp;<a href="#" class="icon-trash" title="{{ _('Uninstall Plugin') }}" data-bind="css: {disabled: !$root.enableUninstall($data)}, enable: $root.enableUninstall($data), click: function() { $root.uninstallPlugin($data) }"></a>
</td>
</tr>
</tbody>
@ -58,6 +66,7 @@
<h3>{{ _('Install new Plugins...') }}</h3>
</div>
<div class="modal-body">
{{ pluginmanager_printing() }}
<h4 style="position: relative">
{{ _('... from the <a href="%(url)s" target="_blank">Plugin Repository</a>', url='http://plugins.octoprint.org') }}
<a class="dropdown-toggle pull-right" data-toggle="dropdown" href="#">
@ -94,7 +103,7 @@
</div>
</div>
<div class="span3">
<button class="btn btn-primary btn-block" data-bind="enable: $root.isCompatible($data), css: {disabled: !$root.isCompatible($data)}, click: function() { if ($root.isCompatible($data)) { $root.installFromRepository($data); } else { return false; } }"><i class="icon-add"></i> <span data-bind="text: $root.installButtonText($data)"></span></button>
<button class="btn btn-primary btn-block" data-bind="enable: $root.enableRepoInstall($data), css: {disabled: !$root.enableRepoInstall($data)}, click: function() { if ($root.enableRepoInstall($data)) { $root.installFromRepository($data); } else { return false; } }"><i class="icon-add"></i> <span data-bind="text: $root.installButtonText($data)"></span></button>
</div>
</div>
</div>

View file

@ -43,6 +43,8 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
def refresh_checks(name, plugin):
self._refresh_configured_checks = True
self._send_client_message("update_versions")
self._plugin_lifecycle_manager.add_callback("enabled", refresh_checks)
self._plugin_lifecycle_manager.add_callback("disabled", refresh_checks)
@ -86,12 +88,76 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
}
def on_settings_save(self, data):
super(SoftwareUpdatePlugin, self).on_settings_save(data)
octoprint.plugin.SettingsPlugin.on_settings_save(self, data)
self._version_cache_ttl = self._settings.get_int(["cache_ttl"]) * 60
def get_settings_version(self):
return 1
def on_settings_migrate(self, target, current=None):
if current is None:
# there might be some left over data from the time we still persisted everything to settings,
# even the stuff that shouldn't be persisted but always provided by the hook - let's
# clean up
# take care of the octoprint entry
configured_checks = self._settings.get(["checks"], merged=True)
octoprint_check = dict(configured_checks["octoprint"])
if "type" in octoprint_check and not octoprint_check["type"] == "github_commit":
deletables=["current"]
else:
deletables=[]
octoprint_check = self._clean_settings_check("octoprint", octoprint_check, self.get_settings_defaults()["checks"]["octoprint"], delete=deletables, save=False)
configured_checks["octoprint"] = octoprint_check
# and the hooks
update_check_hooks = self._plugin_manager.get_hooks("octoprint.plugin.softwareupdate.check_config")
for name, hook in update_check_hooks.items():
try:
hook_checks = hook()
except:
self._logger.exception("Error while retrieving update information from plugin {name}".format(**locals()))
else:
for key, data in hook_checks.items():
if key in configured_checks:
settings_check = dict(configured_checks[key])
merged = dict_merge(data, settings_check)
if "type" in merged and not merged["type"] == "github_commit":
deletables = ["current", "displayVersion"]
else:
deletables = []
self._clean_settings_check(key, settings_check, data, delete=deletables, save=False)
def _clean_settings_check(self, key, data, defaults, delete=None, save=True):
if delete is None:
delete = []
for k, v in data.items():
if k in defaults and defaults[k] == data[k]:
del data[k]
for k in delete:
if k in data:
del data[k]
dummy_defaults = dict(plugins=dict())
dummy_defaults["plugins"][self._identifier] = dict(checks=dict())
dummy_defaults["plugins"][self._identifier]["checks"][key] = defaults
if len(data):
self._settings.set(["checks", key], data, defaults=dummy_defaults)
else:
self._settings.set(["checks", key], None, defaults=dummy_defaults)
if save:
self._settings.save()
return data
#~~ BluePrint API
@octoprint.plugin.BlueprintPlugin.route("/check", methods=["GET"])
@restricted_access
def check_for_update(self):
if "check" in flask.request.values:
check_targets = map(str.strip, flask.request.values["check"].split(","))
@ -101,7 +167,7 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
if "force" in flask.request.values and flask.request.values["force"] in octoprint.settings.valid_boolean_trues:
force = True
else:
force=False
force = False
try:
information, update_available, update_possible = self.get_current_versions(check_targets=check_targets, force=force)
@ -116,10 +182,10 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
def perform_update(self):
if self._printer.is_printing() or self._printer.is_paused():
# do not update while a print job is running
flask.make_response("Printer is currently printing or paused", 409)
return flask.make_response("Printer is currently printing or paused", 409)
if not "application/json" in flask.request.headers["Content-Type"]:
flask.make_response("Expected content-type JSON", 400)
return flask.make_response("Expected content-type JSON", 400)
json_data = flask.request.json
@ -128,9 +194,8 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
else:
check_targets = None
if "force" in json_data:
from octoprint.settings import valid_boolean_trues
force = (json_data["force"] in valid_boolean_trues)
if "force" in json_data and json_data["force"] in octoprint.settings.valid_boolean_trues:
force = True
else:
force = False
@ -378,15 +443,14 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
# persist the new version if necessary for check type
if check["type"] == "github_commit":
checks = self._settings.get(["checks"], merged=True)
if target in checks:
# TODO make this cleaner, right now it saves too much to disk
checks[target]["current"] = target_version
self._settings.set(["checks"], checks)
dummy_default = dict(plugins=dict())
dummy_default["plugins"][self._identifier] = dict(checks=dict())
dummy_default["plugins"][self._identifier]["checks"][target] = dict(current=None)
self._settings.set(["checks", target, "current"], target_version, defaults=dummy_default)
# we have to save here (even though that makes us save quite often) since otherwise the next
# load will overwrite our changes we just made
self._settings.save()
# we have to save here (even though that makes us save quite often) since otherwise the next
# load will overwrite our changes we just made
self._settings.save()
return target_error, target_result

View file

@ -44,9 +44,7 @@ $(function() {
};
self._showPopup = function(options, eventListeners) {
if (self.popup !== undefined) {
self.popup.remove();
}
self._closePopup();
self.popup = new PNotify(options);
if (eventListeners) {
@ -65,6 +63,12 @@ $(function() {
}
};
self._closePopup = function() {
if (self.popup !== undefined) {
self.popup.remove();
}
};
self.showPluginSettings = function() {
self._copyConfig();
self.configurationDialog.modal();
@ -89,6 +93,86 @@ $(function() {
self.config_cacheTtl(self.settings.settings.plugins.softwareupdate.cache_ttl());
};
self.fromCheckResponse = function(data, ignoreSeen, showIfNothingNew) {
var versions = [];
_.each(data.information, function(value, key) {
value["key"] = key;
if (!value.hasOwnProperty("displayName") || value.displayName == "") {
value.displayName = value.key;
}
if (!value.hasOwnProperty("displayVersion") || value.displayVersion == "") {
value.displayVersion = value.information.local.name;
}
versions.push(value);
});
self.versions.updateItems(versions);
if (data.status == "updateAvailable" || data.status == "updatePossible") {
var text = gettext("There are updates available for the following components:");
text += "<ul>";
_.each(self.versions.items(), function(update_info) {
if (update_info.updateAvailable) {
var displayName = update_info.key;
if (update_info.hasOwnProperty("displayName")) {
displayName = update_info.displayName;
}
text += "<li>" + displayName + (update_info.updatePossible ? " <i class=\"icon-ok\"></i>" : "") + "</li>";
}
});
text += "</ul>";
text += "<small>" + gettext("Those components marked with <i class=\"icon-ok\"></i> can be updated directly.") + "</small>";
var options = {
title: gettext("Update Available"),
text: text,
hide: false
};
var eventListeners = {};
if (data.status == "updatePossible" && self.loginState.isAdmin()) {
// if user is admin, add action buttons
options["confirm"] = {
confirm: true,
buttons: [{
text: gettext("Ignore"),
click: function() {
self._markNotificationAsSeen(data.information);
self._showPopup({
text: gettext("You can make this message display again via \"Settings\" > \"SoftwareUpdate\" > \"Check for update now\"")
});
}
}, {
text: gettext("Update now"),
addClass: "btn-primary",
click: self.update
}]
};
options["buttons"] = {
closer: false,
sticker: false
};
}
if (ignoreSeen || !self._hasNotificationBeenSeen(data.information)) {
self._showPopup(options, eventListeners);
}
} else if (data.status == "current") {
if (showIfNothingNew) {
self._showPopup({
title: gettext("Everything is up-to-date"),
hide: false,
type: "success"
});
} else {
self._closePopup();
}
}
};
self.performCheck = function(showIfNothingNew, force, ignoreSeen) {
if (!self.loginState.isUser()) return;
@ -102,79 +186,7 @@ $(function() {
type: "GET",
dataType: "json",
success: function(data) {
var versions = [];
_.each(data.information, function(value, key) {
value["key"] = key;
if (!value.hasOwnProperty("displayName") || value.displayName == "") {
value.displayName = value.key;
}
if (!value.hasOwnProperty("displayVersion") || value.displayVersion == "") {
value.displayVersion = value.information.local.name;
}
versions.push(value);
});
self.versions.updateItems(versions);
if (data.status == "updateAvailable" || data.status == "updatePossible") {
var text = gettext("There are updates available for the following components:");
text += "<ul>";
_.each(self.versions.items(), function(update_info) {
if (update_info.updateAvailable) {
var displayName = update_info.key;
if (update_info.hasOwnProperty("displayName")) {
displayName = update_info.displayName;
}
text += "<li>" + displayName + (update_info.updatePossible ? " <i class=\"icon-ok\"></i>" : "") + "</li>";
}
});
text += "</ul>";
text += "<small>" + gettext("Those components marked with <i class=\"icon-ok\"></i> can be updated directly.") + "</small>";
var options = {
title: gettext("Update Available"),
text: text,
hide: false
};
var eventListeners = {};
if (data.status == "updatePossible" && self.loginState.isAdmin()) {
// if user is admin, add action buttons
options["confirm"] = {
confirm: true,
buttons: [{
text: gettext("Ignore"),
click: function() {
self._markNotificationAsSeen(data.information);
self._showPopup({
text: gettext("You can make this message display again via \"Settings\" > \"SoftwareUpdate\" > \"Check for update now\"")
});
}
}, {
text: gettext("Update now"),
addClass: "btn-primary",
click: self.update
}]
};
options["buttons"] = {
closer: false,
sticker: false
};
}
if (ignoreSeen || !self._hasNotificationBeenSeen(data.information)) {
self._showPopup(options, eventListeners);
}
} else if (data.status == "current" && showIfNothingNew) {
self._showPopup({
title: gettext("Everything is up-to-date"),
hide: false,
type: "success"
});
}
self.fromCheckResponse(data, ignoreSeen, showIfNothingNew);
}
});
};
@ -421,6 +433,10 @@ $(function() {
self.updateInProgress = false;
break;
}
case "update_versions": {
self.performCheck();
break;
}
}
if (options != undefined) {
@ -432,4 +448,4 @@ $(function() {
// view model class, parameters for constructor, container to bind to
ADDITIONAL_VIEWMODELS.push([SoftwareUpdateViewModel, ["loginStateViewModel", "printerStateViewModel", "settingsViewModel"], document.getElementById("settings_plugin_softwareupdate")]);
});
});

View file

@ -210,7 +210,22 @@ class Server():
set_preprocessors=set_preprocessors)
return dict(settings=plugin_settings)
def settings_plugin_pre_init(name, implementation):
if not isinstance(implementation, octoprint.plugin.SettingsPlugin):
return
settings_version = implementation.get_settings_version()
settings_migrator = implementation.on_settings_migrate
if settings_version is not None and settings_migrator is not None:
stored_version = implementation._settings.get_int(["_config_version"])
if stored_version is None or stored_version < settings_version:
settings_migrator(settings_version, stored_version)
implementation._settings.set_int(["_config_version"], settings_version)
implementation._settings.save()
pluginManager.implementation_inject_factories=[octoprint_plugin_inject_factory, settings_plugin_inject_factory]
pluginManager.implementation_pre_inits=[settings_plugin_pre_init]
pluginManager.initialize_implementations()
pluginManager.log_all_plugins()
@ -674,6 +689,20 @@ class Server():
base_folder = settings().getBaseFolder("generated")
# clean the folder
if settings().getBoolean(["devel", "webassets", "clean_on_startup"]):
import shutil
for entry in ("webassets", ".webassets-cache"):
path = os.path.join(base_folder, entry)
self._logger.debug("Deleting {path}...".format(**locals()))
if os.path.isdir(path):
shutil.rmtree(path, ignore_errors=True)
elif os.path.isfile(path):
try:
os.remove(path)
except:
self._logger.exception("Exception while trying to delete {entry} from {base_folder}".format(**locals()))
AdjustedEnvironment = type(Environment)(Environment.__name__, (Environment,), dict(
resolver_class=util.flask.PluginAssetResolver
))
@ -754,16 +783,7 @@ class Server():
if len(less_app) == 0:
less_app = ["empty"]
js_libs_bundle = Bundle(*js_libs, output="webassets/packed_libs.js")
if settings().getBoolean(["devel", "webassets", "minify"]):
js_app_bundle = Bundle(*js_app, output="webassets/packed_app.js", filters="rjsmin")
else:
js_app_bundle = Bundle(*js_app, output="webassets/packed_app.js")
css_libs_bundle = Bundle(*css_libs, output="webassets/packed_libs.css")
css_app_bundle = Bundle(*css_app, output="webassets/packed_app.css")
from webassets.filter import register_filter
from webassets.filter import register_filter, Filter
from webassets.filter.cssrewrite.base import PatternRewriter
import re
class LessImportRewrite(PatternRewriter):
@ -782,7 +802,24 @@ class Server():
return "{import_with_options}\"{import_url}\";".format(**locals())
class JsDelimiterBundle(Filter):
name = "js_delimiter_bundler"
options = {}
def input(self, _in, out, **kwargs):
out.write(_in.read())
out.write("\n;\n")
register_filter(LessImportRewrite)
register_filter(JsDelimiterBundle)
js_libs_bundle = Bundle(*js_libs, output="webassets/packed_libs.js", filters="js_delimiter_bundler")
if settings().getBoolean(["devel", "webassets", "minify"]):
js_app_bundle = Bundle(*js_app, output="webassets/packed_app.js", filters="rjsmin, js_delimiter_bundler")
else:
js_app_bundle = Bundle(*js_app, output="webassets/packed_app.js", filters="js_delimiter_bundler")
css_libs_bundle = Bundle(*css_libs, output="webassets/packed_libs.css")
css_app_bundle = Bundle(*css_app, output="webassets/packed_app.css")
all_less_bundle = Bundle(*less_app, output="webassets/packed_app.less", filters="less_importrewrite")

View file

@ -26,6 +26,8 @@ import octoprint.util
@api.route("/settings", methods=["GET"])
def getSettings():
logger = logging.getLogger(__name__)
s = settings()
connectionOptions = get_connection_options()
@ -118,7 +120,7 @@ def getSettings():
for name in gcode_scripts:
data["scripts"]["gcode"][name] = s.loadScript("gcode", name, source=True)
def process_plugin_result(name, plugin, result):
def process_plugin_result(name, result):
if result:
if not "plugins" in data:
data["plugins"] = dict()
@ -126,9 +128,17 @@ def getSettings():
del result["__enabled"]
data["plugins"][name] = result
octoprint.plugin.call_plugin(octoprint.plugin.SettingsPlugin,
"on_settings_load",
callback=process_plugin_result)
for plugin in octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.SettingsPlugin):
try:
result = plugin.on_settings_load()
process_plugin_result(plugin._identifier, result)
except TypeError:
logger.warn("Could not load settings for plugin {name} ({version}) since it called super(...)".format(name=plugin._plugin_name, version=plugin._plugin_version))
logger.warn("in a way which has issues due to OctoPrint's dynamic reloading after plugin operations.")
logger.warn("Please contact the plugin's author and ask to update the plugin to use a direct call like")
logger.warn("octoprint.plugin.SettingsPlugin.on_settings_load(self) instead.")
except:
logger.exception("Could not load settings for plugin {name} ({version})".format(version=plugin._plugin_version, name=plugin._plugin_name))
return jsonify(data)
@ -137,6 +147,8 @@ def getSettings():
@restricted_access
@admin_permission.require(403)
def setSettings():
logger = logging.getLogger(__name__)
if not "application/json" in request.headers["Content-Type"]:
return make_response("Expected content-type JSON", 400)
@ -235,8 +247,15 @@ def setSettings():
for plugin in octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.SettingsPlugin):
plugin_id = plugin._identifier
if plugin_id in data["plugins"]:
plugin.on_settings_save(data["plugins"][plugin_id])
try:
plugin.on_settings_save(data["plugins"][plugin_id])
except TypeError:
logger.warn("Could not save settings for plugin {name} ({version}) since it called super(...)".format(name=plugin._plugin_name, version=plugin._plugin_version))
logger.warn("in a way which has issues due to OctoPrint's dynamic reloading after plugin operations.")
logger.warn("Please contact the plugin's author and ask to update the plugin to use a direct call like")
logger.warn("octoprint.plugin.SettingsPlugin.on_settings_save(self, data) instead.")
except:
logger.exception("Could not save settings for plugin {name} ({version})".format(version=plugin._plugin_version, name=plugin._plugin_name))
if s.save():
eventManager().fire(Events.SETTINGS_UPDATED)

View file

@ -251,7 +251,8 @@ default_settings = {
},
"webassets": {
"minify": False,
"bundle": True
"bundle": True,
"clean_on_startup": True
},
"virtualPrinter": {
"enabled": False,
@ -853,7 +854,7 @@ class Settings(object):
return results
def getInt(self, path, config=None, defaults=None, preprocessors=None):
value = self.get(path, defaults=defaults, preprocessors=preprocessors)
value = self.get(path, config=config, defaults=defaults, preprocessors=preprocessors)
if value is None:
return None
@ -930,14 +931,15 @@ class Settings(object):
#~~ setter
def set(self, path, value, force=False, defaults=None, preprocessors=None):
def set(self, path, value, force=False, defaults=None, config=None, preprocessors=None):
if len(path) == 0:
return
if self._mtime is not None and self.last_modified != self._mtime:
self.load()
config = self._config
if config is None:
config = self._config
if defaults is None:
defaults = default_settings
if preprocessors is None:
@ -973,9 +975,9 @@ class Settings(object):
config[key] = value
self._dirty = True
def setInt(self, path, value, force=False, defaults=None, preprocessors=None):
def setInt(self, path, value, force=False, defaults=None, config=None, preprocessors=None):
if value is None:
self.set(path, None, force=force, defaults=defaults, preprocessors=preprocessors)
self.set(path, None, config=config, force=force, defaults=defaults, preprocessors=preprocessors)
return
try:
@ -984,11 +986,11 @@ class Settings(object):
self._logger.warn("Could not convert %r to a valid integer when setting option %r" % (value, path))
return
self.set(path, intValue, force)
self.set(path, intValue, config=config, force=force, defaults=defaults, preprocessors=preprocessors)
def setFloat(self, path, value, force=False, defaults=None, preprocessors=None):
def setFloat(self, path, value, force=False, defaults=None, config=None, preprocessors=None):
if value is None:
self.set(path, None, force=force, defaults=defaults, preprocessors=preprocessors)
self.set(path, None, config=config, force=force, defaults=defaults, preprocessors=preprocessors)
return
try:
@ -997,15 +999,15 @@ class Settings(object):
self._logger.warn("Could not convert %r to a valid integer when setting option %r" % (value, path))
return
self.set(path, floatValue, force)
self.set(path, floatValue, config=config, force=force, defaults=defaults, preprocessors=preprocessors)
def setBoolean(self, path, value, force=False, defaults=None, preprocessors=None):
def setBoolean(self, path, value, force=False, defaults=None, config=None, preprocessors=None):
if value is None or isinstance(value, bool):
self.set(path, value, force=force, defaults=defaults, preprocessors=preprocessors)
self.set(path, value, config=config, force=force, defaults=defaults, preprocessors=preprocessors)
elif value.lower() in valid_boolean_trues:
self.set(path, True, force=force, defaults=defaults, preprocessors=preprocessors)
self.set(path, True, config=config, force=force, defaults=defaults, preprocessors=preprocessors)
else:
self.set(path, False, force=force, defaults=defaults, preprocessors=preprocessors)
self.set(path, False, config=config, force=force, defaults=defaults, preprocessors=preprocessors)
def setBaseFolder(self, type, path, force=False):
if type not in default_settings["folder"].keys():

View file

@ -54,7 +54,7 @@ class SlicerException(SlicingException):
Identifier of the slicer for which the exception was raised.
"""
def __init__(self, slicer, *args, **kwargs):
super(SlicingException, self).__init__(*args, **kwargs)
SlicingException.__init__(self, *args, **kwargs)
self.slicer = slicer
class SlicerNotConfigured(SlicerException):
@ -62,7 +62,7 @@ class SlicerNotConfigured(SlicerException):
Raised if a slicer is not yet configured but must be configured to proceed.
"""
def __init__(self, slicer, *args, **kwargs):
super(SlicerException, self).__init__(slicer, *args, **kwargs)
SlicerException.__init__(self, slicer, *args, **kwargs)
self.message = "Slicer not configured: {slicer}".format(slicer=slicer)
class UnknownSlicer(SlicerException):
@ -70,7 +70,7 @@ class UnknownSlicer(SlicerException):
Raised if a slicer is unknown.
"""
def __init__(self, slicer, *args, **kwargs):
super(SlicerException, self).__init__(slicer, *args, **kwargs)
SlicerException.__init__(self, slicer, *args, **kwargs)
self.message = "No such slicer: {slicer}".format(slicer=slicer)
class ProfileException(BaseException):
@ -86,7 +86,7 @@ class ProfileException(BaseException):
Identifier of the profile for which the exception was raised.
"""
def __init__(self, slicer, profile, *args, **kwargs):
super(BaseException, self).__init__(*args, **kwargs)
BaseException.__init__(self, *args, **kwargs)
self.slicer = slicer
self.profile = profile
@ -95,7 +95,7 @@ class UnknownProfile(ProfileException):
Raised if a slicing profile does not exist but must exist to proceed.
"""
def __init__(self, slicer, profile, *args, **kwargs):
super(ProfileException, self).__init__(slicer, profile, *args, **kwargs)
ProfileException.__init__(self, slicer, profile, *args, **kwargs)
self.message = "Profile {profile} for slicer {slicer} does not exist".format(profile=profile, slicer=slicer)
class ProfileAlreadyExists(ProfileException):
@ -103,5 +103,5 @@ class ProfileAlreadyExists(ProfileException):
Raised if a slicing profile already exists and must not be overwritten.
"""
def __init__(self, slicer, profile, *args, **kwargs):
super(ProfileException, self).__init__(slicer, profile, *args, **kwargs)
self.message = "Profile {profile} for slicer {slicer} already exists".format(profile=profile, slicer=slicer)
ProfileException.__init__(self, slicer, profile, *args, **kwargs)
self.message = "Profile {profile} for slicer {slicer} already exists".format(profile=profile, slicer=slicer)