PMGR: Support for plugin notices
Evaluates json data file as e.g. published on plugins.octoprint.org/notices.json and displays notices for plugins installed and matching (optional) version lists.
This commit is contained in:
parent
ea3b7ab1da
commit
52633d0433
3 changed files with 452 additions and 28 deletions
|
|
@ -16,6 +16,7 @@ from octoprint.util.pip import LocalPipCaller, UnknownPip
|
|||
|
||||
from flask import jsonify, make_response
|
||||
from flask.ext.babel import gettext
|
||||
from collections import OrderedDict
|
||||
|
||||
import logging
|
||||
import sarge
|
||||
|
|
@ -24,6 +25,9 @@ import requests
|
|||
import re
|
||||
import os
|
||||
import pkg_resources
|
||||
import copy
|
||||
import dateutil.parser
|
||||
import time
|
||||
|
||||
class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
|
||||
octoprint.plugin.TemplatePlugin,
|
||||
|
|
@ -53,12 +57,19 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
|
|||
self._repository_cache_path = None
|
||||
self._repository_cache_ttl = 0
|
||||
|
||||
self._notices = dict()
|
||||
self._notices_available = False
|
||||
self._notices_cache_path = None
|
||||
self._notices_cache_ttl = 0
|
||||
|
||||
self._console_logger = None
|
||||
|
||||
def initialize(self):
|
||||
self._console_logger = logging.getLogger("octoprint.plugins.pluginmanager.console")
|
||||
self._repository_cache_path = os.path.join(self.get_plugin_data_folder(), "plugins.json")
|
||||
self._repository_cache_ttl = self._settings.get_int(["repository_ttl"]) * 60
|
||||
self._notices_cache_path = os.path.join(self.get_plugin_data_folder(), "notices.json")
|
||||
self._notices_cache_ttl = self._settings.get_int(["notices_ttl"]) * 60
|
||||
|
||||
self._pip_caller = LocalPipCaller(force_user=self._settings.get_boolean(["pip_force_user"]))
|
||||
self._pip_caller.on_log_call = self._log_call
|
||||
|
|
@ -84,6 +95,7 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
|
|||
self._console_logger.propagate = False
|
||||
|
||||
self._repository_available = self._fetch_repository_from_disk()
|
||||
self._notices_available = self._fetch_notices_from_disk()
|
||||
|
||||
##~~ SettingsPlugin
|
||||
|
||||
|
|
@ -91,6 +103,8 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
|
|||
return dict(
|
||||
repository="http://plugins.octoprint.org/plugins.json",
|
||||
repository_ttl=24*60,
|
||||
notices="http://plugins.octoprint.org/notices.json",
|
||||
notices_ttl=6*60,
|
||||
pip_args=None,
|
||||
pip_force_user=False,
|
||||
dependency_links=False,
|
||||
|
|
@ -101,6 +115,7 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
|
|||
octoprint.plugin.SettingsPlugin.on_settings_save(self, data)
|
||||
|
||||
self._repository_cache_ttl = self._settings.get_int(["repository_ttl"]) * 60
|
||||
self._notices_cache_ttl = self._settings.get_int(["notices_ttl"]) * 60
|
||||
self._pip_caller.force_user = self._settings.get_boolean(["pip_force_user"])
|
||||
|
||||
##~~ AssetPlugin
|
||||
|
|
@ -192,6 +207,10 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
|
|||
if refresh_repository:
|
||||
self._repository_available = self._refresh_repository()
|
||||
|
||||
refresh_notices = request.values.get("refresh_notices", "false") in valid_boolean_trues
|
||||
if refresh_notices:
|
||||
self._notices_available = self._refresh_notices()
|
||||
|
||||
def view():
|
||||
return jsonify(plugins=self._get_plugins(),
|
||||
repository=dict(
|
||||
|
|
@ -217,6 +236,8 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
|
|||
hash.update(repr(self._get_plugins()))
|
||||
hash.update(str(self._repository_available))
|
||||
hash.update(repr(self._repository_plugins))
|
||||
hash.update(str(self._notices_available))
|
||||
hash.update(repr(self._notices))
|
||||
hash.update(repr(safe_mode))
|
||||
return hash.hexdigest()
|
||||
|
||||
|
|
@ -225,7 +246,7 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
|
|||
|
||||
return with_revalidation_checking(etag_factory=lambda *args, **kwargs: etag(),
|
||||
condition=lambda *args, **kwargs: condition(),
|
||||
unless=lambda: refresh_repository)(view)()
|
||||
unless=lambda: refresh_repository or refresh_notices)(view)()
|
||||
|
||||
def on_api_command(self, command, data):
|
||||
if not admin_permission.can():
|
||||
|
|
@ -368,7 +389,12 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
|
|||
|
||||
self._plugin_manager.log_all_plugins()
|
||||
|
||||
result = dict(result=True, url=url, needs_restart=needs_restart, needs_refresh=needs_refresh, was_reinstalled=new_plugin_key in all_plugins_before or reinstall is not None, plugin=self._to_external_representation(new_plugin))
|
||||
result = dict(result=True,
|
||||
url=url,
|
||||
needs_restart=needs_restart,
|
||||
needs_refresh=needs_refresh,
|
||||
was_reinstalled=new_plugin_key in all_plugins_before or reinstall is not None,
|
||||
plugin=self._to_external_plugin(new_plugin))
|
||||
self._send_result_notification("install", result)
|
||||
return jsonify(result)
|
||||
|
||||
|
|
@ -448,7 +474,7 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
|
|||
|
||||
self._plugin_manager.reload_plugins()
|
||||
|
||||
result = dict(result=True, needs_restart=needs_restart, needs_refresh=needs_refresh, plugin=self._to_external_representation(plugin))
|
||||
result = dict(result=True, needs_restart=needs_restart, needs_refresh=needs_refresh, plugin=self._to_external_plugin(plugin))
|
||||
self._send_result_notification("uninstall", result)
|
||||
return jsonify(result)
|
||||
|
||||
|
|
@ -473,9 +499,15 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
|
|||
self._logger.exception(u"Problem toggling enabled state of {name}: {reason}".format(name=plugin.key, reason=e.reason))
|
||||
result = dict(result=False, reason=e.reason)
|
||||
except octoprint.plugin.core.PluginNeedsRestart:
|
||||
result = dict(result=True, needs_restart=True, needs_refresh=True, plugin=self._to_external_representation(plugin))
|
||||
result = dict(result=True,
|
||||
needs_restart=True,
|
||||
needs_refresh=True,
|
||||
plugin=self._to_external_plugin(plugin))
|
||||
else:
|
||||
result = dict(result=True, needs_restart=needs_restart_api, needs_refresh=needs_refresh_api, plugin=self._to_external_representation(plugin))
|
||||
result = dict(result=True,
|
||||
needs_restart=needs_restart_api,
|
||||
needs_refresh=needs_refresh_api,
|
||||
plugin=self._to_external_plugin(plugin))
|
||||
|
||||
self._send_result_notification(command, result)
|
||||
return jsonify(result)
|
||||
|
|
@ -572,7 +604,6 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
|
|||
return self._refresh_repository(repo_data=repo_data)
|
||||
|
||||
def _fetch_repository_from_url(self):
|
||||
import requests
|
||||
repository_url = self._settings.get(["repository"])
|
||||
try:
|
||||
r = requests.get(repository_url)
|
||||
|
|
@ -602,7 +633,7 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
|
|||
octoprint_version = self._get_octoprint_version(base=True)
|
||||
|
||||
def map_repository_entry(entry):
|
||||
result = dict(entry)
|
||||
result = copy.deepcopy(entry)
|
||||
|
||||
if not "follow_dependency_links" in result:
|
||||
result["follow_dependency_links"] = False
|
||||
|
|
@ -624,6 +655,69 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
|
|||
self._repository_plugins = map(map_repository_entry, repo_data)
|
||||
return True
|
||||
|
||||
def _fetch_notices_from_disk(self):
|
||||
notice_data = None
|
||||
if os.path.isfile(self._notices_cache_path):
|
||||
import time
|
||||
mtime = os.path.getmtime(self._notices_cache_path)
|
||||
if mtime + self._notices_cache_ttl >= time.time() > mtime:
|
||||
try:
|
||||
import json
|
||||
with open(self._notices_cache_path) as f:
|
||||
notice_data = json.load(f)
|
||||
self._logger.info("Loaded notices from disk, was still valid")
|
||||
except:
|
||||
self._logger.exception("Error while loading notices from {}".format(self._notices_cache_path))
|
||||
|
||||
return self._refresh_notices(notice_data=notice_data)
|
||||
|
||||
def _fetch_notices_from_url(self):
|
||||
notices_url = self._settings.get(["notices"])
|
||||
try:
|
||||
r = requests.get(notices_url)
|
||||
self._logger.info("Loaded plugin notices data from {}".format(notices_url))
|
||||
except Exception as e:
|
||||
self._logger.exception("Could not fetch notices from {notices_url}: {message}".format(notices_url=notices_url, message=str(e)))
|
||||
return None
|
||||
|
||||
notice_data = r.json()
|
||||
|
||||
try:
|
||||
import json
|
||||
with octoprint.util.atomic_write(self._notices_cache_path, "wb") as f:
|
||||
json.dump(notice_data, f)
|
||||
except Exception as e:
|
||||
self._logger.exception("Error while saving notices to {}: {}".format(self._notices_cache_path, str(e)))
|
||||
return notice_data
|
||||
|
||||
def _refresh_notices(self, notice_data=None):
|
||||
if notice_data is None:
|
||||
notice_data = self._fetch_notices_from_url()
|
||||
if notice_data is None:
|
||||
return False
|
||||
|
||||
notices = dict()
|
||||
for notice in notice_data:
|
||||
if not "plugin" in notice or not "text" in notice or not "date" in notice:
|
||||
continue
|
||||
|
||||
key = notice["plugin"]
|
||||
|
||||
try:
|
||||
parsed_date = dateutil.parser.parse(notice["date"])
|
||||
notice["timestamp"] = parsed_date.timetuple()
|
||||
except Exception as e:
|
||||
self._logger.warn("Error while parsing date {!r} for plugin notice "
|
||||
"of plugin {}, ignoring notice: {}".format(notice["date"], key, str(e)))
|
||||
continue
|
||||
|
||||
if not key in notices:
|
||||
notices[key] = []
|
||||
notices[key].append(notice)
|
||||
|
||||
self._notices = notices
|
||||
return True
|
||||
|
||||
def _is_octoprint_compatible(self, octoprint_version, compatibility_entries):
|
||||
"""
|
||||
Tests if the current ``octoprint_version`` is compatible to any of the provided ``compatibility_entries``.
|
||||
|
|
@ -694,14 +788,14 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
|
|||
|
||||
hidden = self._settings.get(["hidden"])
|
||||
result = []
|
||||
for name, plugin in plugins.items():
|
||||
if name in hidden:
|
||||
for key, plugin in plugins.items():
|
||||
if key in hidden:
|
||||
continue
|
||||
result.append(self._to_external_representation(plugin))
|
||||
result.append(self._to_external_plugin(plugin))
|
||||
|
||||
return result
|
||||
|
||||
def _to_external_representation(self, plugin):
|
||||
def _to_external_plugin(self, plugin):
|
||||
return dict(
|
||||
key=plugin.key,
|
||||
name=plugin.name,
|
||||
|
|
@ -720,9 +814,37 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
|
|||
pending_disable=((plugin.enabled or getattr(plugin, "safe_mode_enabled", False)) and plugin.key in self._pending_disable),
|
||||
pending_install=(self._plugin_manager.is_plugin_marked(plugin.key, "installed")),
|
||||
pending_uninstall=(self._plugin_manager.is_plugin_marked(plugin.key, "uninstalled")),
|
||||
origin=plugin.origin.type
|
||||
origin=plugin.origin.type,
|
||||
notifications = self._get_notifications(plugin)
|
||||
)
|
||||
|
||||
def _get_notifications(self, plugin):
|
||||
key = plugin.key
|
||||
if key not in self._notices:
|
||||
return
|
||||
|
||||
plugin_notifications = self._notices.get(key, [])
|
||||
|
||||
def filter_versions(notification):
|
||||
return "text" in notification and "date" in notification and \
|
||||
("versions" not in notification or plugin.version in notification["versions"])
|
||||
|
||||
def map_notification(notification):
|
||||
return self._to_external_notification(key, notification)
|
||||
|
||||
return filter(lambda x: x is not None,
|
||||
map(map_notification,
|
||||
filter(filter_versions,
|
||||
plugin_notifications)))
|
||||
|
||||
def _to_external_notification(self, key, notification):
|
||||
return dict(key=key,
|
||||
date=time.mktime(notification["timestamp"]),
|
||||
text=notification["text"],
|
||||
link=notification.get("link"),
|
||||
versions=notification.get("versions", []),
|
||||
important=notification.get("important", False))
|
||||
|
||||
__plugin_name__ = "Plugin Manager"
|
||||
__plugin_author__ = "Gina Häußge"
|
||||
__plugin_url__ = "http://docs.octoprint.org/en/master/bundledplugins/pluginmanager.html"
|
||||
|
|
|
|||
|
|
@ -10,7 +10,20 @@
|
|||
};
|
||||
|
||||
OctoPrintPluginManagerClient.prototype.get = function(refresh, opts) {
|
||||
return this.base.get(this.base.getSimpleApiUrl("pluginmanager") + ((refresh) ? "?refresh_repository=true" : ""), opts);
|
||||
var refresh_repo, refresh_notices;
|
||||
if (_.isPlainObject(refresh)) {
|
||||
refresh_repo = refresh.repo || false;
|
||||
refresh_notices = refresh.notices || false;
|
||||
} else {
|
||||
refresh_repo = refresh;
|
||||
refresh_notices = false;
|
||||
}
|
||||
|
||||
var query = [];
|
||||
if (refresh_repo) query.push("refresh_repository=true");
|
||||
if (refresh_notices) query.push("refresh_notices=true");
|
||||
|
||||
return this.base.get(this.base.getSimpleApiUrl("pluginmanager") + ((query.length) ? "?" + query.join("&") : ""), opts);
|
||||
};
|
||||
|
||||
OctoPrintPluginManagerClient.prototype.getWithRefresh = function(opts) {
|
||||
|
|
@ -79,6 +92,8 @@ $(function() {
|
|||
|
||||
self.config_repositoryUrl = ko.observable();
|
||||
self.config_repositoryTtl = ko.observable();
|
||||
self.config_noticesUrl = ko.observable();
|
||||
self.config_noticesTtl = ko.observable();
|
||||
self.config_pipAdditionalArgs = ko.observable();
|
||||
self.config_pipForceUser = ko.observable();
|
||||
|
||||
|
|
@ -185,6 +200,20 @@ $(function() {
|
|||
});
|
||||
|
||||
self.notifications = [];
|
||||
self.noticeNotifications = [];
|
||||
self.hiddenNoticeNotifications = {};
|
||||
self.noticeCount = ko.observable(0);
|
||||
|
||||
self.noticeCountText = ko.pureComputed(function() {
|
||||
var count = self.noticeCount();
|
||||
if (count == 0) {
|
||||
return gettext("There are no plugin notices. Great!");
|
||||
} else if (count == 1) {
|
||||
return gettext("There is a plugin notice for one of your installed plugins.");
|
||||
} else {
|
||||
return _.sprintf(gettext("There are %(count)d plugin notices for one or more of your installed plugins."), {count: count});
|
||||
}
|
||||
});
|
||||
|
||||
self.enableManagement = ko.pureComputed(function() {
|
||||
return !self.printerState.isPrinting();
|
||||
|
|
@ -280,19 +309,36 @@ $(function() {
|
|||
return false;
|
||||
};
|
||||
|
||||
self.fromResponse = function(data) {
|
||||
self._fromPluginsResponse(data.plugins);
|
||||
self._fromRepositoryResponse(data.repository);
|
||||
self._fromPipResponse(data.pip);
|
||||
self.fromResponse = function(data, options) {
|
||||
self._fromPluginsResponse(data.plugins, options);
|
||||
self._fromRepositoryResponse(data.repository, options);
|
||||
self._fromPipResponse(data.pip, options);
|
||||
|
||||
self.safeMode(data.safe_mode || false);
|
||||
};
|
||||
|
||||
self._fromPluginsResponse = function(data) {
|
||||
self._fromPluginsResponse = function(data, options) {
|
||||
var evalNotices = options.eval_notices || false;
|
||||
var ignoreNoticeHidden = options.ignore_notice_hidden || false;
|
||||
var ignoreNoticeIgnored = options.ignore_notice_ignored || false;
|
||||
|
||||
if (evalNotices) self._removeAllNoticeNotifications();
|
||||
|
||||
var installedPlugins = [];
|
||||
var noticeCount = 0;
|
||||
_.each(data, function(plugin) {
|
||||
installedPlugins.push(plugin.key);
|
||||
|
||||
if (evalNotices && plugin.notifications && plugin.notifications.length) {
|
||||
_.each(plugin.notifications, function(notification) {
|
||||
noticeCount++;
|
||||
if (!ignoreNoticeIgnored && self._isNoticeNotificationIgnored(plugin.key, notification.date)) return;
|
||||
if (!ignoreNoticeHidden && self._isNoticeNotificationHidden(plugin.key, notification.date)) return;
|
||||
self._showPluginNotification(plugin, notification);
|
||||
});
|
||||
}
|
||||
});
|
||||
if (evalNotices) self.noticeCount(noticeCount);
|
||||
self.installedPlugins(installedPlugins);
|
||||
self.plugins.updateItems(data);
|
||||
};
|
||||
|
|
@ -324,13 +370,28 @@ $(function() {
|
|||
}
|
||||
};
|
||||
|
||||
self.requestData = function(includeRepo) {
|
||||
self.requestData = function(options) {
|
||||
if (!self.loginState.isAdmin()) {
|
||||
return;
|
||||
}
|
||||
|
||||
OctoPrint.plugins.pluginmanager.get(includeRepo)
|
||||
.done(self.fromResponse);
|
||||
if (!_.isPlainObject(options)) {
|
||||
options = {
|
||||
refresh_repo: options,
|
||||
refresh_notices: false,
|
||||
eval_notices: false
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
options.refresh_repo = options.refresh_repo || false;
|
||||
options.refresh_notices = options.refresh_notices || false;
|
||||
options.eval_notices = options.eval_notices || false;
|
||||
|
||||
OctoPrint.plugins.pluginmanager.get({repo: options.refresh_repo, notices: options.refresh_notices})
|
||||
.done(function(data) {
|
||||
self.fromResponse(data, options);
|
||||
});
|
||||
};
|
||||
|
||||
self.togglePlugin = function(data) {
|
||||
|
|
@ -344,7 +405,9 @@ $(function() {
|
|||
|
||||
if (data.key == "pluginmanager") return;
|
||||
|
||||
var onSuccess = self.requestData,
|
||||
var onSuccess = function() {
|
||||
self.requestData();
|
||||
},
|
||||
onError = function() {
|
||||
new PNotify({
|
||||
title: gettext("Something went wrong"),
|
||||
|
|
@ -479,7 +542,9 @@ $(function() {
|
|||
self._markWorking(gettext("Uninstalling plugin..."), _.sprintf(gettext("Uninstalling plugin \"%(name)s\""), {name: data.name}));
|
||||
|
||||
OctoPrint.plugins.pluginmanager.uninstall(data.key)
|
||||
.done(self.requestData)
|
||||
.done(function() {
|
||||
self.requestData();
|
||||
})
|
||||
.fail(function() {
|
||||
new PNotify({
|
||||
title: gettext("Something went wrong"),
|
||||
|
|
@ -497,8 +562,23 @@ $(function() {
|
|||
if (!self.loginState.isAdmin()) {
|
||||
return;
|
||||
}
|
||||
self.requestData({refresh_repo: true});
|
||||
};
|
||||
|
||||
self.requestData(true);
|
||||
self.refreshNotices = function() {
|
||||
if (!self.loginState.isAdmin()) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.requestData({refresh_notices: true, eval_notices: true, ignore_notice_hidden: true, ignore_notice_ignored: true});
|
||||
};
|
||||
|
||||
self.reshowNotices = function() {
|
||||
if (!self.loginState.isAdmin()) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.requestData({eval_notices: true, ignore_notice_hidden: true, ignore_notice_ignored: true});
|
||||
};
|
||||
|
||||
self.showPluginSettings = function() {
|
||||
|
|
@ -519,6 +599,18 @@ $(function() {
|
|||
repositoryTtl = null;
|
||||
}
|
||||
|
||||
var notices = self.config_noticesUrl();
|
||||
if (notices != undefined && notices.trim() == "") {
|
||||
notices = null;
|
||||
}
|
||||
|
||||
var noticesTtl;
|
||||
try {
|
||||
noticesTtl = parseInt(self.config_noticesTtl());
|
||||
} catch (ex) {
|
||||
noticesTtl = null;
|
||||
}
|
||||
|
||||
var pipArgs = self.config_pipAdditionalArgs();
|
||||
if (pipArgs != undefined && pipArgs.trim() == "") {
|
||||
pipArgs = null;
|
||||
|
|
@ -529,6 +621,8 @@ $(function() {
|
|||
pluginmanager: {
|
||||
repository: repository,
|
||||
repository_ttl: repositoryTtl,
|
||||
notices: notices,
|
||||
notices_ttl: noticesTtl,
|
||||
pip_args: pipArgs,
|
||||
pip_force_user: self.config_pipForceUser()
|
||||
}
|
||||
|
|
@ -537,13 +631,15 @@ $(function() {
|
|||
self.settingsViewModel.saveData(data, function() {
|
||||
self.configurationDialog.modal("hide");
|
||||
self._copyConfig();
|
||||
self.refreshRepository();
|
||||
self.requestData({refresh_repo: true, refresh_notices: true, eval_notices: true});
|
||||
});
|
||||
};
|
||||
|
||||
self._copyConfig = function() {
|
||||
self.config_repositoryUrl(self.settingsViewModel.settings.plugins.pluginmanager.repository());
|
||||
self.config_repositoryTtl(self.settingsViewModel.settings.plugins.pluginmanager.repository_ttl());
|
||||
self.config_noticesUrl(self.settingsViewModel.settings.plugins.pluginmanager.notices());
|
||||
self.config_noticesTtl(self.settingsViewModel.settings.plugins.pluginmanager.notices_ttl());
|
||||
self.config_pipAdditionalArgs(self.settingsViewModel.settings.plugins.pluginmanager.pip_args());
|
||||
self.config_pipForceUser(self.settingsViewModel.settings.plugins.pluginmanager.pip_force_user());
|
||||
};
|
||||
|
|
@ -706,13 +802,184 @@ $(function() {
|
|||
}
|
||||
};
|
||||
|
||||
self.showPluginNotifications = function(plugin) {
|
||||
if (!plugin.notifications || plugin.notifications.length == 0) return;
|
||||
|
||||
self._removeAllNoticeNotificationsForPlugin(plugin.key);
|
||||
_.each(plugin.notifications, function(notification) {
|
||||
self._showPluginNotification(plugin, notification);
|
||||
});
|
||||
};
|
||||
|
||||
self.showPluginNotificationsLinkText = function(plugins) {
|
||||
if (!plugins.notifications || plugins.notifications.length == 0) return;
|
||||
|
||||
var count = plugins.notifications.length;
|
||||
var importantCount = _.filter(plugins.notifications, function(notification) { return notification.important }).length;
|
||||
if (count > 1) {
|
||||
if (importantCount) {
|
||||
return _.sprintf(gettext("There are %(count)d notices (%(important)d marked as important) available regarding this plugin - click to show!"), {count: count, important: importantCount});
|
||||
} else {
|
||||
return _.sprintf(gettext("There are %(count)d notices available regarding this plugin - click to show!"), {count: count});
|
||||
}
|
||||
} else {
|
||||
if (importantCount) {
|
||||
return gettext("There is an important notice available regarding this plugin - click to show!");
|
||||
} else {
|
||||
return gettext("There is a notice available regarding this plugin - click to show!");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
self._showPluginNotification = function(plugin, notification) {
|
||||
var name = plugin.name;
|
||||
var version = plugin.version;
|
||||
|
||||
var important = notification.important;
|
||||
var link = notification.link;
|
||||
|
||||
var title;
|
||||
if (important) {
|
||||
title = _.sprintf(gettext("Important notice regarding plugin \"%(name)s\""), {name: name});
|
||||
} else {
|
||||
title = _.sprintf(gettext("Notice regarding plugin \"%(name)s\""), {name: name});
|
||||
}
|
||||
|
||||
var text = "";
|
||||
|
||||
if (notification.versions && notification.versions.length > 0) {
|
||||
var versions = _.map(notification.versions, function(v) { return (v == version) ? "<strong>" + v + "</strong>" : v; }).join(", ");
|
||||
text += "<small>" + _.sprintf(gettext("Affected versions: %(versions)s"), {versions: versions}) + "</small>";
|
||||
} else {
|
||||
text += "<small>" + gettext("Affected versions: all") + "</small>";
|
||||
}
|
||||
|
||||
text += "<p>" + notification.text + "</p>";
|
||||
if (link) {
|
||||
text += "<p><a href='" + link + "' target='_blank'>" + gettext("Read more...") + "</a></p>";
|
||||
}
|
||||
|
||||
var beforeClose = function(notification) {
|
||||
if (!self.noticeNotifications[plugin.key]) return;
|
||||
self.noticeNotifications[plugin.key] = _.without(self.noticeNotifications[plugin.key], notification);
|
||||
};
|
||||
|
||||
var options = {
|
||||
title: title,
|
||||
text: text,
|
||||
type: (important) ? "error" : "notice",
|
||||
before_close: beforeClose,
|
||||
hide: false,
|
||||
confirm: {
|
||||
confirm: true,
|
||||
buttons: [{
|
||||
text: gettext("Later"),
|
||||
click: function(notice) {
|
||||
self._hideNoticeNotification(plugin.key, notification.date);
|
||||
notice.remove();
|
||||
notice.get().trigger("pnotify.cancel", notice);
|
||||
}
|
||||
}, {
|
||||
text: gettext("Mark read"),
|
||||
click: function(notice) {
|
||||
self._ignoreNoticeNotification(plugin.key, notification.date);
|
||||
notice.remove();
|
||||
notice.get().trigger("pnotify.cancel", notice);
|
||||
}
|
||||
}]
|
||||
},
|
||||
buttons: {
|
||||
sticker: false,
|
||||
closer: false
|
||||
}
|
||||
};
|
||||
|
||||
if (!self.noticeNotifications[plugin.key]) {
|
||||
self.noticeNotifications[plugin.key] = [];
|
||||
}
|
||||
self.noticeNotifications[plugin.key].push(new PNotify(options));
|
||||
};
|
||||
|
||||
self._removeAllNoticeNotifications = function() {
|
||||
_.each(_.keys(self.noticeNotifications), function(key) {
|
||||
self._removeAllNoticeNotificationsForPlugin(key);
|
||||
});
|
||||
};
|
||||
|
||||
self._removeAllNoticeNotificationsForPlugin = function(key) {
|
||||
if (!self.noticeNotifications[key] || !self.noticeNotifications[key].length) return;
|
||||
_.each(self.noticeNotifications[key], function(notification) {
|
||||
notification.remove();
|
||||
});
|
||||
};
|
||||
|
||||
self._hideNoticeNotification = function(key, date) {
|
||||
if (!self.hiddenNoticeNotifications[key]) {
|
||||
self.hiddenNoticeNotifications[key] = [];
|
||||
}
|
||||
if (!_.contains(self.hiddenNoticeNotifications[key], date)) {
|
||||
self.hiddenNoticeNotifications[key].push(date);
|
||||
}
|
||||
};
|
||||
|
||||
self._isNoticeNotificationHidden = function(key, date) {
|
||||
if (!self.hiddenNoticeNotifications[key]) return false;
|
||||
return _.any(_.map(self.hiddenNoticeNotifications[key], function(d) { return date == d; }));
|
||||
};
|
||||
|
||||
var noticeLocalStorageKey = "plugin.pluginmanager.seen_notices";
|
||||
self._ignoreNoticeNotification = function(key, date) {
|
||||
if (!Modernizr.localstorage)
|
||||
return false;
|
||||
if (!self.loginState.isAdmin())
|
||||
return false;
|
||||
|
||||
var currentString = localStorage[noticeLocalStorageKey];
|
||||
var current;
|
||||
if (currentString === undefined) {
|
||||
current = {};
|
||||
} else {
|
||||
current = JSON.parse(currentString);
|
||||
}
|
||||
if (!current[self.loginState.username()]) {
|
||||
current[self.loginState.username()] = {};
|
||||
}
|
||||
if (!current[self.loginState.username()][key]) {
|
||||
current[self.loginState.username()][key] = [];
|
||||
}
|
||||
|
||||
if (!_.contains(current[self.loginState.username()][key], date)) {
|
||||
current[self.loginState.username()][key].push(date);
|
||||
localStorage[noticeLocalStorageKey] = JSON.stringify(current);
|
||||
}
|
||||
};
|
||||
|
||||
self._isNoticeNotificationIgnored = function(key, date) {
|
||||
if (!Modernizr.localstorage)
|
||||
return false;
|
||||
|
||||
if (localStorage[noticeLocalStorageKey] == undefined)
|
||||
return false;
|
||||
|
||||
var knownData = JSON.parse(localStorage[noticeLocalStorageKey]);
|
||||
|
||||
if (!self.loginState.isAdmin())
|
||||
return true;
|
||||
|
||||
var userData = knownData[self.loginState.username()];
|
||||
if (userData === undefined)
|
||||
return false;
|
||||
|
||||
return userData[key] && _.contains(userData[key], date);
|
||||
};
|
||||
|
||||
self.onBeforeBinding = function() {
|
||||
self.settings = self.settingsViewModel.settings;
|
||||
};
|
||||
|
||||
self.onUserLoggedIn = function(user) {
|
||||
if (user.admin) {
|
||||
self.requestData();
|
||||
self.requestData({eval_notices: true});
|
||||
} else {
|
||||
self.onUserLoggedOut();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,8 @@
|
|||
<tbody data-bind="foreach: plugins.paginatedItems">
|
||||
<tr>
|
||||
<td class="settings_plugin_plugin_manager_plugins_name">
|
||||
<div data-bind="css: {muted: !enabled}"><span data-bind="text: name"></span> <span data-bind="visible: version">(<span data-bind="text: version"></span>)</span> <i title="{{ _('Bundled with OctoPrint') }}" class="icon-th-large" data-bind="visible: bundled"></i> <i class="icon-lock" title="{{ _('Cannot be uninstalled through OctoPrint') }}" data-bind="visible: !managable"></i> <i title="{{ _('Restart of OctoPrint needed for changes to take effect') }}" class="icon-refresh" data-bind="visible: pending_enable || pending_disable || pending_install || pending_uninstall"></i> <i title="{{ _('Pending install') }}" class="icon-plus" data-bind="visible: pending_install"></i> <i title="{{ _('Pending uninstall') }}" class="icon-minus" data-bind="visible: pending_uninstall"></i> <i title="{{ _('Disabled due to safe mode') }}" class="icon-medkit" data-bind="visible: safe_mode_victim"></i></div>
|
||||
<div data-bind="css: {muted: !enabled}"><span data-bind="text: name"></span> <span data-bind="visible: version">(<span data-bind="text: version"></span>)</span> <i title="{{ _('Bundled with OctoPrint') }}" class="icon-th-large" data-bind="visible: bundled"></i> <i class="icon-lock" title="{{ _('Cannot be uninstalled through OctoPrint') }}" data-bind="visible: !managable"></i> <i title="{{ _('Restart of OctoPrint needed for changes to take effect') }}" class="icon-refresh" data-bind="visible: pending_enable || pending_disable || pending_install || pending_uninstall"></i> <i title="{{ _('Pending install') }}" class="icon-plus" data-bind="visible: pending_install"></i> <i title="{{ _('Pending uninstall') }}" class="icon-minus" data-bind="visible: pending_uninstall"></i> <i title="{{ _('Disabled due to safe mode') }}" class="icon-medkit" data-bind="visible: safe_mode_victim"></i> <i class="icon-exclamation-sign" title="{{ _('There are notices available regarding this plugin') }}" data-bind="visible: notifications && notifications.length"></i></div>
|
||||
<div data-bind="visible: notifications && notifications.length"><a href="javascript:void(0)" class="text-error" style="text-decoration: underline" data-bind="click: function() { $root.showPluginNotifications($data) }, text: $root.showPluginNotificationsLinkText($data)"></a></div>
|
||||
<div><small class="muted" data-bind="text: description"> </small></div>
|
||||
<div data-bind="css: {muted: !enabled}">
|
||||
<small data-bind="visible: url"><i class="icon-home"></i> <a data-bind="attr: {href: url}" target="_blank" rel="noreferrer noopener">{{ _('Homepage') }}</a></small>
|
||||
|
|
@ -72,7 +73,7 @@
|
|||
<div class="muted" data-bind="visible: pipAvailable()">
|
||||
<div>
|
||||
<small>
|
||||
<a href="#" class="muted" onclick="$(this).children('i.toggle-arrow').toggleClass('icon-caret-right icon-caret-down').parent().parent().parent().next().slideToggle('fast')">
|
||||
<a href="javascript:void(0)" class="muted" onclick="$(this).children('i.toggle-arrow').toggleClass('icon-caret-right icon-caret-down').parent().parent().parent().next().slideToggle('fast')">
|
||||
<i class="toggle-arrow icon-caret-right"></i> Using pip of "<span data-bind="text: pipPython"></span>", Version <span data-bind="text: pipVersion"></span>
|
||||
</a>
|
||||
</small>
|
||||
|
|
@ -86,6 +87,22 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="muted">
|
||||
<div>
|
||||
<small>
|
||||
<a href="javascript:void(0)" class="muted" onclick="$(this).children('i.toggle-arrow').toggleClass('icon-caret-right icon-caret-down').parent().parent().parent().next().slideToggle('fast')">
|
||||
<i class="toggle-arrow icon-caret-right"></i> <span data-bind="text: noticeCountText"></span>
|
||||
</a>
|
||||
</small>
|
||||
</div>
|
||||
<div class="hide">
|
||||
<small>
|
||||
<!-- ko if: noticeCount() > 0 --><a href="javascript:void(0)" data-bind="click: function() { reshowNotices(); }">{{ _('Reshow current notices') }}</a> ·<!-- /ko -->
|
||||
<a href="javascript:void(0)" data-bind="click: function() { refreshNotices(); }">{{ _('Refresh notices from repository') }}</a>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="settings_plugin_pluginmanager_workingdialog" class="modal hide fade">
|
||||
<div class="modal-header">
|
||||
<h3 data-bind="text: workingTitle"></h3>
|
||||
|
|
@ -237,6 +254,24 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<legend>{{ _('Notices configuration') }}</legend>
|
||||
|
||||
<div class="control-group" title="{{ _('URL of the Notices source to use. You should normally not have to change this.') }}">
|
||||
<label class="control-label">{{ _('Notices URL') }}</label>
|
||||
<div class="controls">
|
||||
<input type="text" class="input-block-level" data-bind="value: config_noticesUrl">
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group" title="{{ _('How long to cache notices, in minutes. You should normally not have to change this.') }}">
|
||||
<label class="control-label">{{ _('Notices cache TTL') }}</label>
|
||||
<div class="controls">
|
||||
<div class="input-append">
|
||||
<input type="number" class="input-mini" data-bind="value: config_noticesTtl">
|
||||
<span class="add-on">min</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<legend>{{ _('pip configuration') }}</legend>
|
||||
|
||||
<div class="control-group" title="{{ _('Additional arguments for the pip command. You should normally not have to change this.') }}">
|
||||
|
|
|
|||
Loading…
Reference in a new issue