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:
Gina Häußge 2015-12-10 14:41:46 +01:00
parent 8a7d234571
commit 8149a3b4a8
9 changed files with 128 additions and 37 deletions

View file

@ -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()

View file

@ -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}

View file

@ -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"]
]);
});

View file

@ -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;
}
}
}

View file

@ -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">&times;</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>

View file

@ -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>

View file

@ -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

View file

@ -917,6 +917,10 @@ textarea.block {
margin-right: auto;
}
.ui-pnotify a {
text-decoration: underline;
}
/** Styles for Bootstrap Slider */
.slider {