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:
parent
35d9775e51
commit
c3ad1d3691
6 changed files with 111 additions and 44 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -53,4 +53,10 @@ table {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
.configurelink {
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"> </small></div>
|
||||
</td>
|
||||
<td class="settings_plugin_announcements_channels_actions">
|
||||
|
|
|
|||
Loading…
Reference in a new issue