Include release notes in update notification
... and confirmation dialog and settings dialog.
github_release fetches release notes link from github. Check configurations
can always set individual release notes links via the new `release_notes`
property. The URL also supports placeholders `{octoprint_version}`,
`{target_version}` and `{target_name}`. A custom release note URL
hence could be configured by a plugin via
def get_update_information(self):
return dict(
myplugin=dict(
[...]
release_notes="https://me.github.io/MyPlugin/my/custom/releasenotes.html#version_{target_version}"
[...]
)
)
and if a new release "1.3.4" was now to be released would be displayed to the user as
https://me.github.io/MyPlugin/my/custom/releasenotes.html#version_1.3.4
The same of course is possible via config.yaml:
plugins:
softwareupdate:
checks:
myplugin:
release_notes: 'https://me.github.io/MyPlugin/my/custom/releasenotes.html#version_{target_version}'
This commit is contained in:
parent
8a7d234571
commit
8149a3b4a8
9 changed files with 128 additions and 37 deletions
|
|
@ -335,7 +335,7 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
|
|||
@restricted_access
|
||||
def check_for_update(self):
|
||||
if "check" in flask.request.values:
|
||||
check_targets = map(str.strip, flask.request.values["check"].split(","))
|
||||
check_targets = map(lambda x: x.strip(), flask.request.values["check"].split(","))
|
||||
else:
|
||||
check_targets = None
|
||||
|
||||
|
|
@ -365,7 +365,7 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
|
|||
json_data = flask.request.json
|
||||
|
||||
if "check" in json_data:
|
||||
check_targets = map(str.strip, json_data["check"])
|
||||
check_targets = map(lambda x: x.strip(), json_data["check"])
|
||||
else:
|
||||
check_targets = None
|
||||
|
||||
|
|
@ -425,7 +425,7 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
|
|||
self._logger.warn("Unknown update check type for target {}: {}".format(target, check.get("type", "<n/a>")))
|
||||
continue
|
||||
|
||||
target_information = dict_merge(dict(local=dict(name="unknown", value="unknown"), remote=dict(name="unknown", value="unknown")), target_information)
|
||||
target_information = dict_merge(dict(local=dict(name="unknown", value="unknown"), remote=dict(name="unknown", value="unknown", release_notes=None)), target_information)
|
||||
|
||||
update_available = update_available or target_update_available
|
||||
update_possible = update_possible or (target_update_possible and target_update_available)
|
||||
|
|
@ -435,12 +435,25 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
|
|||
local_name = target_information["local"]["name"]
|
||||
local_value = target_information["local"]["value"]
|
||||
|
||||
release_notes = None
|
||||
if target_information and target_information["remote"] and target_information["remote"]["value"]:
|
||||
if "release_notes" in populated_check and populated_check["release_notes"]:
|
||||
release_notes = populated_check["release_notes"]
|
||||
elif "release_notes" in target_information["remote"]:
|
||||
release_notes = target_information["remote"]["release_notes"]
|
||||
|
||||
if release_notes:
|
||||
release_notes = release_notes.format(octoprint_version=octoprint_version,
|
||||
target_name=target_information["remote"]["name"],
|
||||
target_version=target_information["remote"]["value"])
|
||||
|
||||
information[target] = dict(updateAvailable=target_update_available,
|
||||
updatePossible=target_update_possible,
|
||||
information=target_information,
|
||||
displayName=populated_check["displayName"],
|
||||
displayVersion=populated_check["displayVersion"].format(octoprint_version=octoprint_version, local_name=local_name, local_value=local_value),
|
||||
check=populated_check)
|
||||
check=populated_check,
|
||||
releaseNotes=release_notes)
|
||||
|
||||
if self._version_cache_dirty:
|
||||
self._save_version_cache()
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
td.settings_plugin_softwareupdate_column_update{width:16px}
|
||||
td.settings_plugin_softwareupdate_column_update{width:16px}.softwareupdate_notification ul{margin:10px 0 10px 25px}.softwareupdate_notification ul .name{text-overflow:ellipsis;white-space:nowrap;overflow:hidden;display:block}
|
||||
|
|
@ -7,6 +7,8 @@ $(function() {
|
|||
self.settings = parameters[2];
|
||||
self.popup = undefined;
|
||||
|
||||
self.forceUpdate = false;
|
||||
|
||||
self.updateInProgress = false;
|
||||
self.waitingForRestart = false;
|
||||
self.restartTimeout = undefined;
|
||||
|
|
@ -21,6 +23,7 @@ $(function() {
|
|||
self.config_checkType = ko.observable();
|
||||
|
||||
self.configurationDialog = $("#settings_plugin_softwareupdate_configurationdialog");
|
||||
self.confirmationDialog = $("#softwareupdate_confirmation_dialog");
|
||||
|
||||
self.config_availableCheckTypes = [
|
||||
{"key": "github_release", "name": gettext("Release")},
|
||||
|
|
@ -49,6 +52,10 @@ $(function() {
|
|||
5
|
||||
);
|
||||
|
||||
self.availableAndPossible = ko.computed(function() {
|
||||
return _.filter(self.versions.items(), function(info) { return info.updateAvailable && info.updatePossible; });
|
||||
});
|
||||
|
||||
self.onUserLoggedIn = function() {
|
||||
self.performCheck();
|
||||
};
|
||||
|
|
@ -118,6 +125,11 @@ $(function() {
|
|||
if (!value.hasOwnProperty("displayVersion") || value.displayVersion == "") {
|
||||
value.displayVersion = value.information.local.name;
|
||||
}
|
||||
if (!value.hasOwnProperty("releaseNotes") || value.releaseNotes == "") {
|
||||
value.releaseNotes = undefined;
|
||||
}
|
||||
|
||||
value.fullName = _.sprintf(gettext("%(displayName)s: %(displayVersion)s"), value);
|
||||
|
||||
versions.push(value);
|
||||
});
|
||||
|
|
@ -143,22 +155,24 @@ $(function() {
|
|||
}
|
||||
|
||||
if (data.status == "updateAvailable" || data.status == "updatePossible") {
|
||||
var text = gettext("There are updates available for the following components:");
|
||||
var text = "<div class='softwareupdate_notification'>" + gettext("There are updates available for the following components:");
|
||||
|
||||
text += "<ul>";
|
||||
text += "<ul class='icons-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 += "<li>"
|
||||
+ "<i class='icon-li " + (update_info.updatePossible ? "icon-ok" : "icon-remove")+ "'></i>"
|
||||
+ "<span class='name' title='" + update_info.fullName + "'>" + update_info.fullName + "</span>"
|
||||
+ (update_info.releaseNotes ? "<a href=\"" + update_info.releaseNotes + "\" target=\"_blank\">" + gettext("Release Notes") + "</a>" : "")
|
||||
+ "</li>";
|
||||
}
|
||||
});
|
||||
text += "</ul>";
|
||||
|
||||
text += "<small>" + gettext("Those components marked with <i class=\"icon-ok\"></i> can be updated directly.") + "</small>";
|
||||
|
||||
text += "</div>";
|
||||
|
||||
var options = {
|
||||
title: gettext("Update Available"),
|
||||
text: text,
|
||||
|
|
@ -257,7 +271,7 @@ $(function() {
|
|||
return result;
|
||||
};
|
||||
|
||||
self.performUpdate = function(force) {
|
||||
self.performUpdate = function(force, items) {
|
||||
self.updateInProgress = true;
|
||||
|
||||
var options = {
|
||||
|
|
@ -272,12 +286,19 @@ $(function() {
|
|||
};
|
||||
self._showPopup(options);
|
||||
|
||||
var postData = {
|
||||
force: (force == true)
|
||||
};
|
||||
if (items != undefined) {
|
||||
postData.check = items;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: PLUGIN_BASEURL + "softwareupdate/update",
|
||||
type: "POST",
|
||||
dataType: "json",
|
||||
contentType: "application/json; charset=UTF-8",
|
||||
data: JSON.stringify({force: (force == true)}),
|
||||
data: JSON.stringify(postData),
|
||||
error: function() {
|
||||
self.updateInProgress = false;
|
||||
self._showPopup({
|
||||
|
|
@ -300,8 +321,6 @@ $(function() {
|
|||
if (self.updateInProgress) return;
|
||||
if (!self.loginState.isAdmin()) return;
|
||||
|
||||
force = (force == true);
|
||||
|
||||
if (self.printerState.isPrinting()) {
|
||||
self._showPopup({
|
||||
title: gettext("Can't update while printing"),
|
||||
|
|
@ -309,18 +328,18 @@ $(function() {
|
|||
type: "error"
|
||||
});
|
||||
} else {
|
||||
$("#confirmation_dialog .confirmation_dialog_message").text(gettext("This will update your OctoPrint installation and restart the server."));
|
||||
$("#confirmation_dialog .confirmation_dialog_acknowledge").unbind("click");
|
||||
$("#confirmation_dialog .confirmation_dialog_acknowledge").click(function(e) {
|
||||
e.preventDefault();
|
||||
$("#confirmation_dialog").modal("hide");
|
||||
self.performUpdate(force);
|
||||
});
|
||||
$("#confirmation_dialog").modal("show");
|
||||
self.forceUpdate = (force == true);
|
||||
self.confirmationDialog.modal("show");
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
self.confirmUpdate = function() {
|
||||
self.confirmationDialog.hide();
|
||||
self.performUpdate(self.forceUpdate,
|
||||
_.map(self.availableAndPossible(), function(info) { return info.key }));
|
||||
};
|
||||
|
||||
self.onServerDisconnect = function() {
|
||||
if (self.restartTimeout !== undefined) {
|
||||
clearTimeout(self.restartTimeout);
|
||||
|
|
@ -472,5 +491,9 @@ $(function() {
|
|||
}
|
||||
|
||||
// view model class, parameters for constructor, container to bind to
|
||||
ADDITIONAL_VIEWMODELS.push([SoftwareUpdateViewModel, ["loginStateViewModel", "printerStateViewModel", "settingsViewModel"], document.getElementById("settings_plugin_softwareupdate")]);
|
||||
ADDITIONAL_VIEWMODELS.push([
|
||||
SoftwareUpdateViewModel,
|
||||
["loginStateViewModel", "printerStateViewModel", "settingsViewModel"],
|
||||
["#settings_plugin_softwareupdate", "#softwareupdate_confirmation_dialog"]
|
||||
]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,3 +1,17 @@
|
|||
td.settings_plugin_softwareupdate_column_update {
|
||||
width: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.softwareupdate_notification {
|
||||
ul {
|
||||
margin: 10px 0 10px 25px;
|
||||
|
||||
.name {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
<div id="softwareupdate_confirmation_dialog" class="modal hide fade">
|
||||
<div class="modal-header">
|
||||
<a href="#" class="close" data-dismiss="modal" aria-hidden="true">×</a>
|
||||
<h3>{{ _('Are you sure you want to update now?') }}</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
{{ _('This will update the following components and restart the server:') }}
|
||||
</p>
|
||||
<ul>
|
||||
<!-- ko foreach: availableAndPossible -->
|
||||
<li>
|
||||
<span class="name" data-bind="text: fullName, attr: {title: fullName}"></span>
|
||||
<div class="releaseNotes" data-bind="visible: releaseNotes"><a data-bind="attr: {href: releaseNotes}">{{ _('Release Notes') }}</a></div>
|
||||
</li>
|
||||
<!-- /ko -->
|
||||
</ul>
|
||||
<p>
|
||||
{{ _('Be sure to read through any linked release notes, especially those for OctoPrint since they might contain important information you need to know <strong>before</strong> upgrading.') }}
|
||||
</p>
|
||||
<p>
|
||||
{{ _('Are you sure you want to proceed?') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a href="#" class="btn" data-dismiss="modal" aria-hidden="true">{{ _('Cancel') }}</a>
|
||||
<a href="#" class="btn btn-danger" data-bind="click: function() { $root.confirmUpdate(); }">{{ _('Proceed') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -32,7 +32,8 @@
|
|||
<strong data-bind="text: displayName"></strong>: <span data-bind="text: displayVersion"></span> <span data-bind="invisible: updatePossible"><i class="icon-exclamation-sign" title="{{ _('Update not possible, configuration ok?') }}"></i></span><br>
|
||||
<small class="muted">
|
||||
{{ _('Installed:') }} <span data-bind="text: information.local.name"></span><br>
|
||||
{{ _('Available:') }} <span data-bind="text: information.remote.name"></span>
|
||||
{{ _('Available:') }} <span data-bind="text: information.remote.name"></span><br>
|
||||
<span data-bind="visible: releaseNotes">{{ _('Release Notes:') }} <a data-bind="attr: {href: releaseNotes}, text: releaseNotes" target="_blank"></a></span>
|
||||
</small>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
|||
|
|
@ -15,33 +15,40 @@ RELEASE_URL = "https://api.github.com/repos/{user}/{repo}/releases"
|
|||
logger = logging.getLogger("octoprint.plugins.softwareupdate.version_checks.github_release")
|
||||
|
||||
def _get_latest_release(user, repo, include_prerelease=False):
|
||||
nothing = None, None, None
|
||||
r = requests.get(RELEASE_URL.format(user=user, repo=repo))
|
||||
|
||||
from . import log_github_ratelimit
|
||||
log_github_ratelimit(logger, r)
|
||||
|
||||
if not r.status_code == requests.codes.ok:
|
||||
return None, None
|
||||
return nothing
|
||||
|
||||
releases = r.json()
|
||||
|
||||
# sanitize
|
||||
required_fields = {"name", "tag_name", "html_url", "draft", "prerelease", "published_at"}
|
||||
releases = filter(lambda rel: set(rel.keys()) & required_fields == required_fields,
|
||||
releases)
|
||||
|
||||
# filter out prereleases and drafts
|
||||
if include_prerelease:
|
||||
releases = filter(lambda rel: not rel["draft"], releases)
|
||||
else:
|
||||
releases = filter(lambda rel: not rel["prerelease"] and not rel["draft"], releases)
|
||||
releases = filter(lambda rel: not rel["prerelease"] and not rel["draft"],
|
||||
releases)
|
||||
|
||||
if not releases:
|
||||
return None, None
|
||||
return nothing
|
||||
|
||||
# sort by date
|
||||
comp = lambda a, b: cmp(a["published_at"], b["published_at"])
|
||||
comp = lambda a, b: cmp(a.get("published_at", None), b["published_at"])
|
||||
releases = sorted(releases, cmp=comp)
|
||||
|
||||
# latest release = last in list
|
||||
latest = releases[-1]
|
||||
|
||||
return latest["name"], latest["tag_name"]
|
||||
return latest["name"], latest["tag_name"], latest.get("html_url", None)
|
||||
|
||||
|
||||
def _get_sanitized_version(version_string):
|
||||
|
|
@ -122,14 +129,14 @@ def get_latest(target, check, custom_compare=None):
|
|||
include_prerelease = check.get("prerelease", False)
|
||||
force_base = check.get("force_base", True)
|
||||
|
||||
remote_name, remote_tag = _get_latest_release(check["user"],
|
||||
check["repo"],
|
||||
include_prerelease=include_prerelease)
|
||||
remote_name, remote_tag, release_notes = _get_latest_release(check["user"],
|
||||
check["repo"],
|
||||
include_prerelease=include_prerelease)
|
||||
compare_type = check["release_compare"] if "release_compare" in check else "python"
|
||||
|
||||
information =dict(
|
||||
local=dict(name=current, value=current),
|
||||
remote=dict(name=remote_name, value=remote_tag)
|
||||
remote=dict(name=remote_name, value=remote_tag, release_notes=release_notes)
|
||||
)
|
||||
|
||||
logger.debug("Target: %s, local: %s, remote: %s" % (target, current, remote_tag))
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -917,6 +917,10 @@ textarea.block {
|
|||
margin-right: auto;
|
||||
}
|
||||
|
||||
.ui-pnotify a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/** Styles for Bootstrap Slider */
|
||||
|
||||
.slider {
|
||||
|
|
|
|||
Loading…
Reference in a new issue