Announcements: Various improvements

* Added combined OctoBlog feed, replacing news + spotlight (+
    octoprintonair), added corresponding config migration
  * Subscribe to all registered feeds by default
  * Added config button to announcement reader
  * Added note how to edit announcement subscriptions to notifications
  * Auto-hide announcements on logout
  * Order channels server-side based on new order config setting
This commit is contained in:
Gina Häußge 2017-03-30 12:40:57 +02:00
parent 35d9775e51
commit c3ad1d3691
6 changed files with 111 additions and 44 deletions

View file

@ -18,6 +18,8 @@ import threading
import feedparser
import flask
from collections import OrderedDict
from octoprint.server import admin_permission
from octoprint.server.util.flask import restricted_access, with_revalidation_checking, check_etag
from flask.ext.babel import gettext
@ -44,35 +46,64 @@ class AnnouncementPlugin(octoprint.plugin.AssetPlugin,
# SettingsPlugin
def get_settings_defaults(self):
return dict(channels=dict(_important=dict(name="Important OctoPrint Announcements",
priority=1,
type="rss",
url="http://octoprint.org/feeds/important.xml"),
_news=dict(name="OctoPrint News",
priority=2,
type="rss",
url="http://octoprint.org/feeds/news.xml"),
_releases=dict(name="OctoPrint Release Announcements",
settings = dict(channels=dict(_important=dict(name="Important Announcements",
description="Important announcements about OctoPrint.",
priority=1,
type="rss",
url="http://octoprint.org/feeds/important.xml"),
_releases=dict(name="Release Announcements",
description="Announcements of new releases and release candidates of OctoPrint.",
priority=2,
type="rss",
url="http://octoprint.org/feeds/releases.xml"),
_blog=dict(name="On the OctoBlog",
description="Development news, community spotlights, OctoPrint On Air episodes and more from the official OctoBlog.",
priority=2,
type="rss",
url="http://octoprint.org/feeds/releases.xml"),
_spotlight=dict(name="OctoPrint Community Spotlights",
priority=2,
type="rss",
url="http://octoprint.org/feeds/spotlight.xml"),
_octopi=dict(name="OctoPi Announcements",
priority=2,
type="rss",
url="http://octoprint.org/feeds/octopi.xml"),
_plugins=dict(name="New Plugins in the Repository",
priority=2,
type="rss",
url="http://plugins.octoprint.org/feed.xml")),
enabled_channels=[],
forced_channels=["_important"],
ttl=6*60,
display_limit=3,
summary_limit=300)
url="http://octoprint.org/feeds/octoblog.xml"),
_plugins=dict(name="New Plugins in the Repository",
description="Announcements of new plugins released on the official Plugin Repository.",
priority=2,
type="rss",
url="http://plugins.octoprint.org/feed.xml"),
_octopi=dict(name="OctoPi News",
description="News around OctoPi, the Raspberry Pi image including OctoPrint.",
priority=2,
type="rss",
url="http://octoprint.org/feeds/octopi.xml")),
enabled_channels=[],
forced_channels=["_important"],
channel_order=["_important", "_releases", "_blog", "_plugins", "_octopi"],
ttl=6*60,
display_limit=3,
summary_limit=300)
settings["enabled_channels"] = settings["channels"].keys()
return settings
def get_settings_version(self):
return 1
def on_settings_migrate(self, target, current):
if current is None:
# first version had different default feeds and only _important enabled by default
channels = self._settings.get(["channels"])
if "_news" in channels:
del channels["_news"]
if "_spotlight" in channels:
del channels["_spotlight"]
self._settings.set(["channels"], channels)
enabled = self._settings.get(["enabled_channels"])
add_blog = False
if "_news" in enabled:
add_blog = True
enabled.remove("_news")
if "_spotlight" in enabled:
add_blog = True
enabled.remove("_spotlight")
if add_blog and not "_blog" in enabled:
enabled.append("_blog")
self._settings.set(["enabled_channels"], enabled)
# AssetPlugin
@ -97,7 +128,7 @@ class AnnouncementPlugin(octoprint.plugin.AssetPlugin,
def get_channel_data(self):
from octoprint.settings import valid_boolean_trues
result = dict()
result = []
force = flask.request.values.get("force", "false") in valid_boolean_trues
@ -118,15 +149,17 @@ class AnnouncementPlugin(octoprint.plugin.AssetPlugin,
last = entries[0]["published"]
self._mark_read_until(key, last)
result[key] = dict(channel=data["name"],
result.append(dict(key=key,
channel=data["name"],
url=data["url"],
description=data.get("description", ""),
priority=data.get("priority", 2),
enabled=key in enabled or key in forced,
forced=key in forced,
data=entries,
unread=unread)
unread=unread))
return flask.jsonify(result)
return flask.jsonify(channels=result)
def etag():
import hashlib
@ -141,11 +174,11 @@ class AnnouncementPlugin(octoprint.plugin.AssetPlugin,
return hash.hexdigest()
def condition():
return check_etag(etag())
def condition(lm, etag):
return check_etag(etag)
return with_revalidation_checking(etag_factory=lambda *args, **kwargs: etag(),
condition=lambda *args, **kwargs: condition(),
condition=lambda lm, etag: condition(lm, etag),
unless=lambda: force)(view)()
@octoprint.plugin.BlueprintPlugin.route("/channels/<channel>", methods=["POST"])
@ -208,9 +241,13 @@ class AnnouncementPlugin(octoprint.plugin.AssetPlugin,
with self._cached_channel_configs_mutex:
if self._cached_channel_configs is None or force:
configs = self._settings.get(["channels"], merged=True)
result = dict()
for key, config in configs.items():
if "url" not in config or "name" not in config:
order = self._settings.get(["channel_order"])
all_keys = order + [key for key in sorted(configs.keys()) if not key in order]
result = OrderedDict()
for key in all_keys:
config = configs.get(key)
if config is None or "url" not in config or "name" not in config:
# strip invalid entries
continue
result[self._slugify(key)] = config

View file

@ -1 +1 @@
table td.settings_plugin_announcements_channels_name,table th.settings_plugin_announcements_channels_name{text-overflow:ellipsis;text-align:left}table td.settings_plugin_announcements_channels_actions,table th.settings_plugin_announcements_channels_actions{text-align:center;width:80px}table td.settings_plugin_announcements_channels_actions a,table th.settings_plugin_announcements_channels_actions a{text-decoration:none;color:#000}table td.settings_plugin_announcements_channels_actions a.disabled,table th.settings_plugin_announcements_channels_actions a.disabled{color:#ccc;cursor:default}#plugin_announcements_dialog .unread{font-weight:700}#plugin_announcements_dialog article{padding-right:20px}#plugin_announcements_dialog article.read{opacity:.5}#plugin_announcements_dialog article.read:hover{opacity:1}#plugin_announcements_dialog article .actions{background-color:#f5f5f5;border-radius:2px;padding:2px 5px;margin-top:5px}#plugin_announcements_dialog article .actions .markread{float:right}#plugin_announcements_dialog article .actions a{color:#000}
table td.settings_plugin_announcements_channels_name,table th.settings_plugin_announcements_channels_name{text-overflow:ellipsis;text-align:left}table td.settings_plugin_announcements_channels_actions,table th.settings_plugin_announcements_channels_actions{text-align:center;width:80px}table td.settings_plugin_announcements_channels_actions a,table th.settings_plugin_announcements_channels_actions a{text-decoration:none;color:#000}table td.settings_plugin_announcements_channels_actions a.disabled,table th.settings_plugin_announcements_channels_actions a.disabled{color:#ccc;cursor:default}#plugin_announcements_dialog .unread{font-weight:700}#plugin_announcements_dialog article{padding-right:20px}#plugin_announcements_dialog article.read{opacity:.5}#plugin_announcements_dialog article.read:hover{opacity:1}#plugin_announcements_dialog article .actions{background-color:#f5f5f5;border-radius:2px;padding:2px 5px;margin-top:5px}#plugin_announcements_dialog article .actions .markread{float:right}#plugin_announcements_dialog article .actions a{color:#000}#plugin_announcements_dialog .modal-footer .configurelink{float:left}

View file

@ -120,12 +120,13 @@ $(function() {
};
self.fromResponse = function(data) {
if (!self.loginState.isAdmin()) return;
var currentTab = $("li.active a", self.announcementDialogTabs).attr("href");
var unread = 0;
var channels = [];
_.each(data, function(value, key) {
value.key = key;
_.each(data.channels, function(value) {
value.last = value.data.length ? value.data[0].published : undefined;
value.count = value.data.length;
unread += value.unread;
@ -140,6 +141,8 @@ $(function() {
};
self.showAnnouncementDialog = function(channel) {
if (!self.loginState.isAdmin()) return;
self.announcementDialogContent.scrollTop(0);
if (!self.announcementDialog.hasClass("in")) {
@ -172,6 +175,8 @@ $(function() {
};
self.displayAnnouncements = function(channels) {
if (!self.loginState.isAdmin()) return;
var displayLimit = self.settings.settings.plugins.announcements.display_limit();
var maxLength = self.settings.settings.plugins.announcements.summary_limit();
@ -222,7 +227,7 @@ $(function() {
}
var rest = newItems.length - displayedItems.length;
var text = "<ul>";
var text = "<ul style='margin-top: 10px; margin-bottom: 10px'>";
_.each(displayedItems, function(item) {
var limitedSummary = stripParagraphs(item.summary_without_images.trim());
if (limitedSummary.length > maxLength) {
@ -239,6 +244,8 @@ $(function() {
text += gettext(_.sprintf("... and %(rest)d more.", {rest: rest}));
}
text += "<small>" + gettext("You can edit your announcement subscriptions under Settings > Announcements.") + "</small>";
var options = {
title: channel,
text: text,
@ -284,10 +291,25 @@ $(function() {
});
};
self.hideAnnouncements = function() {
_.each(self.channelNotifications, function(notification, key) {
notification.remove();
});
self.channelNotifications = {};
};
self.configureAnnouncements = function() {
self.settings.show("settings_plugin_announcements");
};
self.onUserLoggedIn = function() {
self.retrieveData();
};
self.onUserLoggedOut = function() {
self.hideAnnouncements();
};
self.onStartup = function() {
self.announcementDialog = $("#plugin_announcements_dialog");
self.announcementDialogContent = $("#plugin_announcements_dialog_content");

View file

@ -53,4 +53,10 @@ table {
}
}
}
.modal-footer {
.configurelink {
float: left;
}
}
}

View file

@ -11,7 +11,7 @@
<!-- ko foreach: {data: channels.items, afterAdd: setupTabLink} -->
<!-- ko if: $data.enabled || $data.forced -->
<li>
<a data-toggle="tab" target="_blank" data-bind="text: $data.channel + ' (' + $data.unread + '/' + $data.count + ')', attr: {href: '#plugin_announcements_dialog_channel_' + $data.key}, css: {unread: $data.unread}"></a>
<a data-toggle="tab" target="_blank" data-bind="text: $data.channel, attr: {href: '#plugin_announcements_dialog_channel_' + $data.key, title: $data.description}, css: {unread: $data.unread}"></a>
</li>
<!-- /ko -->
<!-- /ko -->
@ -40,6 +40,7 @@
</div>
</div>
<div class="modal-footer">
<button class="btn btn-block" data-dismiss="modal" aria-hidden="true">{{ _('Close') }}</button>
<button class="btn configurelink" data-bind="click: configureAnnouncements"><i class="icon-wrench"></i></button>
<button class="btn" data-dismiss="modal" aria-hidden="true">{{ _('Close') }}</button>
</div>
</div>

View file

@ -10,7 +10,8 @@
<tbody data-bind="foreach: channels.paginatedItems">
<tr>
<td class="settings_plugin_announcements_channels_name">
<div data-bind="text: channel, css: {muted: !enabled}"></div>
<div data-bind="css: {muted: !enabled}"><strong data-bind="text: channel"></strong></div>
<div data-bind="text: description, css: {muted: !enabled}"></div>
<div><small class="muted" data-bind="text: url">&nbsp;</small></div>
</td>
<td class="settings_plugin_announcements_channels_actions">