diff --git a/.gitignore b/.gitignore index 87a55bc2..f1335930 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ src/octoprint/templates/_data/AUTHORS.md src/octoprint/templates/_data/CHANGELOG.md -src/octoprint/templates/_data/SPONSORS.md +src/octoprint/templates/_data/SUPPORTERS.md src/octoprint/templates/_data/THIRDPARTYLICENSES.md devtools diff --git a/README.md b/README.md index ccb813ce..ae4f6369 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,9 @@ The documentation is located at [docs.octoprint.org](http://docs.octoprint.org). The official plugin repository can be reached at [plugins.octoprint.org](http://plugins.octoprint.org). +OctoPrint's development wouldn't be possible without the [financial support by its community](http://octoprint.org/support-octoprint/). +If you enjoy OctoPrint, please consider becoming a regular supporter! + ![Screenshot](http://i.imgur.com/dF3noFp.png) You are currently looking at the source code repository of OctoPrint. If you already installed it @@ -26,7 +29,7 @@ Contributions of all kinds are welcome, not only in the form of code but also wi [official documentation](http://docs.octoprint.org/) or [the public wiki](https://github.com/foosel/OctoPrint/wiki), support of other users in the [bug tracker](https://github.com/foosel/OctoPrint/issues), [the Mailinglist](https://groups.google.com/group/octoprint) or -[the G+ Community](https://plus.google.com/communities/102771308349328485741). +[the G+ Community](https://plus.google.com/communities/102771308349328485741) and also [financially](http://octoprint.org/support-octoprint/). If you think something is bad as it is about OctoPrint or its documentation the way it is, please help in any way to make it better instead of just complaining about it -- this is an Open Source Project diff --git a/SPONSORS.md b/SUPPORTERS.md similarity index 88% rename from SPONSORS.md rename to SUPPORTERS.md index eac40941..0924ead6 100644 --- a/SPONSORS.md +++ b/SUPPORTERS.md @@ -1,4 +1,4 @@ -# Sponsors +# Supporters Development of this version of OctoPrint wouldn't have been possible without [financial support by the community](http://octoprint.org/support-octoprint/) - @@ -14,8 +14,8 @@ thanks to everyone who contributed! * E3D BigBox * Erik de Bruijn * Ernesto Martinez - * Exovite - * georgeroblesjr + * Exovite + * georgeroblesjr * Gregor Luetolf * Kale Stedman * Makespace Madrid @@ -26,6 +26,6 @@ thanks to everyone who contributed! * Samer Najia * Stefan Krister * Sven Mueller - * Tom + * Tom -and 321 more wonderful people pledging on the [Patreon campaign](https://patreon.com/foosel)! \ No newline at end of file +and 321 more wonderful people pledging on the [Patreon campaign](https://patreon.com/foosel)! diff --git a/THIRDPARTYLICENSES.md b/THIRDPARTYLICENSES.md index f181d587..381e182a 100644 --- a/THIRDPARTYLICENSES.md +++ b/THIRDPARTYLICENSES.md @@ -34,10 +34,11 @@ * [Awesome-Slugify](https://pypi.python.org/pypi/awesome-slugify): GPLv3 * [Click](http://click.pocoo.org/): BSD + * [feedparser](https://github.com/kurtmckee/feedparser): BSD * [Flask](http://flask.pocoo.org/): BSD * [Flask-Assets](http://github.com/miracle2k/flask-assets): BSD * [Flask-Babel](http://github.com/mitsuhiko/flask-babel): BSD - * [Flask-Login](https://flask-login.readthedocs.org/en/latest/https://github.com/maxcountryman/flask-login): MIT + * [Flask-Login](https://github.com/maxcountryman/flask-login): MIT * [Flask-Markdown](http://github.com/dcolish/flask-markdown): BSD * [Flask-Principal](http://packages.python.org/Flask-Principal/): MIT * [netaddr](https://github.com/drkjam/netaddr/): BSD diff --git a/setup.py b/setup.py index 2066a24e..3ff3246a 100644 --- a/setup.py +++ b/setup.py @@ -36,7 +36,8 @@ INSTALL_REQUIRES = [ "semantic_version>=2.4.2,<2.5", "psutil>=3.2.1,<3.3", "Click>=6.2,<6.3", - "awesome-slugify>=1.6.5,<1.7" + "awesome-slugify>=1.6.5,<1.7", + "feedparser>=5.2.1,<5.3" ] # Additional requirements for optional install options @@ -114,7 +115,7 @@ def get_cmdclass(): "octoprint/templates/_data": [ "AUTHORS.md", "CHANGELOG.md", - "SPONSORS.md", + "SUPPORTERS.md", "THIRDPARTYLICENSES.md", ] }, cmdclass["build_py"] if "build_py" in cmdclass else _build_py) diff --git a/src/octoprint/plugins/announcements/__init__.py b/src/octoprint/plugins/announcements/__init__.py new file mode 100644 index 00000000..f1407177 --- /dev/null +++ b/src/octoprint/plugins/announcements/__init__.py @@ -0,0 +1,286 @@ +# coding=utf-8 +from __future__ import absolute_import + +__author__ = "Gina Häußge " +__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html' +__copyright__ = "Copyright (C) 2016 The OctoPrint Project - Released under terms of the AGPLv3 License" + + +import octoprint.plugin + +import calendar +import codecs +import os +import re +import time +import threading + +import feedparser +import flask + +from octoprint.server import admin_permission +from octoprint.server.util.flask import restricted_access +from flask.ext.babel import gettext + +class AnnouncementPlugin(octoprint.plugin.AssetPlugin, + octoprint.plugin.SettingsPlugin, + octoprint.plugin.BlueprintPlugin, + octoprint.plugin.StartupPlugin, + octoprint.plugin.TemplatePlugin): + + def __init__(self): + self._cached_channels = dict() + self._cached_channels_mutex = threading.RLock() + + # StartupPlugin + + def on_after_startup(self): + self._fetch_all_channels() + + # 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", + read_until=1449446400), + _releases=dict(name="OctoPrint Release Announcements", + priority=2, + type="rss", + url="http://octoprint.org/feeds/releases.xml", + read_until=1458121176), + _spotlight=dict(name="OctoPrint Community Spotlights", + priority=2, + type="rss", + url="http://octoprint.org/feeds/spotlight.xml", + read_until=1447953971), + _octopi=dict(name="OctoPi Announcements", + priority=2, + type="rss", + url="http://octoprint.org/feeds/octopi.xml", + read_until=1462200600), + _plugins=dict(name="New Plugins in the Repository", + priority=2, + type="rss", + url="http://plugins.octoprint.org/feed.xml", + read_until=1461628800)), + enabled_channels=[], + forced_channels=["_important"], + ttl=6*60, + display_limit=3, + summary_limit=300) + + # AssetPlugin + + def get_assets(self): + return dict(js=["js/announcements.js"], + less=["less/announcements.less"], + css=["css/announcements.css"]) + + # Template Plugin + + def get_template_configs(self): + return [ + dict(type="settings", name=gettext("Announcements"), template="announcements_settings.jinja2", custom_bindings=True), + dict(type="navbar", template="announcements_navbar.jinja2", styles=["display: none"], data_bind="visible: loginState.isAdmin") + ] + + # Blueprint Plugin + + @octoprint.plugin.BlueprintPlugin.route("/channels", methods=["GET"]) + @restricted_access + @admin_permission.require(403) + def get_channel_data(self): + result = dict() + + channel_data = self._fetch_all_channels() + + channel_configs = self._get_channel_configs() + enabled = self._settings.get(["enabled_channels"]) + forced = self._settings.get(["forced_channels"]) + for key, data in channel_configs.items(): + entries = self._to_internal_feed(channel_data.get(key, []), read_until=channel_configs[key].get("read_until", None)) + unread = len(filter(lambda e: not e["read"], entries)) + + result[key] = dict(channel=data["name"], + url=data["url"], + priority=data["priority"], + enabled=key in enabled or key in forced, + forced=key in forced, + data=entries, + unread=unread) + + return flask.jsonify(result) + + @octoprint.plugin.BlueprintPlugin.route("/channels/", methods=["POST"]) + @restricted_access + @admin_permission.require(403) + def channel_command(self, channel): + from octoprint.server.util.flask import get_json_command_from_request + from octoprint.server import NO_CONTENT + + valid_commands = dict(read=["until"], + toggle=[]) + + command, data, response = get_json_command_from_request(flask.request, valid_commands=valid_commands) + if response is not None: + return response + + if command == "read": + current_read_until = None + channel_data = self._settings.get(["channels", channel], merged=True) + if channel_data: + current_read_until = channel_data.get("read_until", None) + + defaults = dict(plugins=dict(announcements=dict(channels=dict()))) + defaults["plugins"]["announcements"]["channels"][channel] = dict(read_until=current_read_until) + + until = data["until"] + self._settings.set(["channels", channel, "read_until"], until, defaults=defaults) + self._settings.save() + + elif command == "toggle": + enabled_channels = list(self._settings.get(["enabled_channels"])) + + if channel in enabled_channels: + enabled_channels.remove(channel) + else: + enabled_channels.append(channel) + + self._settings.set(["enabled_channels"], enabled_channels) + self._settings.save() + + return NO_CONTENT + + # Internal Tools + + def _get_channel_configs(self): + return self._settings.get(["channels"], merged=True) + + def _fetch_all_channels(self): + with self._cached_channels_mutex: + channels = self._get_channel_configs() + enabled = self._settings.get(["enabled_channels"]) + forced = self._settings.get(["forced_channels"]) + + all_channels = dict() + for key, config in channels.items(): + if not key in enabled and not key in forced: + continue + + data = self._get_channel_data(key, config) + if data is not None: + all_channels[key] = data + + self._cached_channels = all_channels + + return self._cached_channels + + def _get_channel_data(self, key, config): + data = self._get_channel_data_from_cache(key, config) + if data is None: + data = self._get_channel_data_from_network(key, config) + return data + + def _get_channel_data_from_cache(self, key, config): + channel_path = os.path.join(self.get_plugin_data_folder(), "{}.cache".format(key)) + + if os.path.exists(channel_path): + if "ttl" in config and isinstance(config["ttl"], int): + ttl = config["ttl"] + else: + ttl = self._settings.get_int(["ttl"]) + + ttl *= 60 + now = time.time() + if os.stat(channel_path).st_mtime + ttl > now: + d = feedparser.parse(channel_path) + self._logger.info("Loaded channel {} from cache".format(key)) + return d + + return None + + def _get_channel_data_from_network(self, key, config): + import requests + + url = config["url"] + try: + r = requests.get(url) + self._logger.info("Loaded channel {} from {}".format(key, config["url"])) + except Exception as e: + self._logger.exception( + "Could not fetch channel {} from {}: {}".format(key, config["url"], str(e))) + return None + + response = r.text + channel_path = os.path.join(self.get_plugin_data_folder(), "{}.cache".format(key)) + with codecs.open(channel_path, mode="w", encoding="utf-8") as f: + f.write(response) + return feedparser.parse(response) + + def _to_internal_feed(self, feed, read_until=None): + result = [] + if "entries" in feed: + for entry in feed["entries"]: + internal_entry = self._to_internal_entry(entry, read_until=read_until) + if internal_entry: + result.append(internal_entry) + return result + + def _to_internal_entry(self, entry, read_until=None): + published = calendar.timegm(entry["published_parsed"]) + + read = False + if read_until is not None: + read = published <= read_until + + return dict(title=entry["title"], + title_without_tags=_strip_tags(entry["title"]), + summary=entry["summary"], + summary_without_images=_strip_images(entry["summary"]), + published=published, + link=entry["link"], + read=read) + + +_image_tag_re = re.compile(r'') +def _strip_images(text): + return _image_tag_re.sub('', text) + + +def _strip_tags(text): + """ + >>> _strip_tags(u"Hello world<img src='foo.jpg'>") + u"Hello world<img src='foo.jpg'>" + >>> _strip_tags(u"> > Foo") + u'> > Foo' + """ + + from HTMLParser import HTMLParser + + class TagStripper(HTMLParser): + + def __init__(self): + HTMLParser.__init__(self) + self._fed = [] + + def handle_data(self, data): + self._fed.append(data) + + def handle_entityref(self, ref): + self._fed.append("&{};".format(ref)) + + def handle_charref(self, ref): + self._fed.append("&#{};".format(ref)) + + def get_data(self): + return "".join(self._fed) + + tag_stripper = TagStripper() + tag_stripper.feed(text) + return tag_stripper.get_data() + + +__plugin_name__ = "Announcement Plugin" +__plugin_implementation__ = AnnouncementPlugin() diff --git a/src/octoprint/plugins/announcements/static/css/announcements.css b/src/octoprint/plugins/announcements/static/css/announcements.css new file mode 100644 index 00000000..08e9a9bd --- /dev/null +++ b/src/octoprint/plugins/announcements/static/css/announcements.css @@ -0,0 +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} \ No newline at end of file diff --git a/src/octoprint/plugins/announcements/static/js/announcements.js b/src/octoprint/plugins/announcements/static/js/announcements.js new file mode 100644 index 00000000..812d7005 --- /dev/null +++ b/src/octoprint/plugins/announcements/static/js/announcements.js @@ -0,0 +1,300 @@ +$(function() { + function AnnouncementsViewModel(parameters) { + var self = this; + + self.loginState = parameters[0]; + self.settings = parameters[1]; + + self.channels = new ItemListHelper( + "plugin.announcements.channels", + { + "channel": function (a, b) { + // sorts ascending + if (a["channel"].toLocaleLowerCase() < b["channel"].toLocaleLowerCase()) return -1; + if (a["channel"].toLocaleLowerCase() > b["channel"].toLocaleLowerCase()) return 1; + return 0; + } + }, + { + }, + "name", + [], + [], + 5 + ); + + self.unread = ko.observable(); + self.hiddenChannels = []; + self.channelNotifications = {}; + + self.announcementDialog = undefined; + self.announcementDialogContent = undefined; + self.announcementDialogTabs = undefined; + + self.setupTabLink = function(item) { + $("a[data-toggle='tab']", item).on("show", self.resetContentScroll); + }; + + self.resetContentScroll = function() { + self.announcementDialogContent.scrollTop(0); + }; + + self.toggleButtonCss = function(data) { + var icon = data.enabled ? "icon-circle" : "icon-circle-blank"; + var disabled = (self.enableToggle(data)) ? "" : " disabled"; + + return icon + disabled; + }; + + self.toggleButtonTitle = function(data) { + return data.forced ? gettext("Cannot be toggled") : (data.enabled ? gettext("Disable Channel") : gettext("Enable Channel")); + }; + + self.enableToggle = function(data) { + return !data.forced; + }; + + self.markRead = function(channel, until) { + if (!self.loginState.isAdmin()) return; + + var url = PLUGIN_BASEURL + "announcements/channels/" + channel; + + var payload = { + command: "read", + until: until + }; + + $.ajax({ + url: url, + type: "POST", + dataType: "json", + data: JSON.stringify(payload), + contentType: "application/json; charset=UTF-8", + success: function() { + self.retrieveData() + } + }) + }; + + self.toggleChannel = function(channel) { + if (!self.loginState.isAdmin()) return; + + var url = PLUGIN_BASEURL + "announcements/channels/" + channel; + + var payload = { + command: "toggle" + }; + + $.ajax({ + url: url, + type: "POST", + dataType: "json", + data: JSON.stringify(payload), + contentType: "application/json; charset=UTF-8", + success: function() { + self.retrieveData() + } + }) + }; + + self.retrieveData = function(force) { + if (!self.loginState.isAdmin()) return; + + var url = PLUGIN_BASEURL + "announcements/channels"; + if (force) { + url += "?force=true"; + } + + $.ajax({ + url: url, + type: "GET", + dataType: "json", + success: function(data) { + self.fromResponse(data); + } + }); + }; + + self.fromResponse = function(data) { + var currentTab = $("li.active a", self.announcementDialogTabs).attr("href"); + + var unread = 0; + var channels = []; + _.each(data, function(value, key) { + value.key = key; + value.last = value.data.length ? value.data[0].published : undefined; + value.count = value.data.length; + unread += value.unread; + channels.push(value); + }); + self.channels.updateItems(channels); + self.unread(unread); + + self.displayAnnouncements(channels); + + self.selectTab(currentTab); + }; + + self.showAnnouncementDialog = function(channel) { + self.announcementDialogContent.scrollTop(0); + + if (!self.announcementDialog.hasClass("in")) { + self.announcementDialog.modal({ + minHeight: function() { return Math.max($.fn.modal.defaults.maxHeight() - 80, 250); } + }).css({ + width: 'auto', + 'margin-left': function() { return -($(this).width() /2); } + }); + } + + var tab = undefined; + if (channel) { + tab = "#plugin_announcements_dialog_channel_" + channel; + } + self.selectTab(tab); + + return false; + }; + + self.selectTab = function(tab) { + if (tab != undefined) { + if (!_.startsWith(tab, "#")) { + tab = "#" + tab; + } + $('a[href="' + tab + '"]', self.announcementDialogTabs).tab("show"); + } else { + $('a:first', self.announcementDialogTabs).tab("show"); + } + }; + + self.displayAnnouncements = function(channels) { + var displayLimit = self.settings.settings.plugins.announcements.display_limit(); + var maxLength = self.settings.settings.plugins.announcements.summary_limit(); + + var cutAfterNewline = function(text) { + text = text.trim(); + + var firstNewlinePos = text.indexOf("\n"); + if (firstNewlinePos > 0) { + text = text.substr(0, firstNewlinePos).trim(); + } + + return text; + }; + + var stripParagraphs = function(text) { + if (_.startsWith(text, "

")) { + text = text.substr("

".length); + } + if (_.endsWith(text, "

")) { + text = text.substr(0, text.length - "

".length); + } + + return text.replace(/<\/p>\s*

/ig, "
"); + }; + + _.each(channels, function(value) { + var key = value.key; + var channel = value.channel; + var priority = value.priority; + var items = value.data; + + if ($.inArray(key, self.hiddenChannels) > -1) { + // channel currently ignored + return; + } + + var newItems = _.filter(items, function(entry) { return !entry.read; }); + if (newItems.length == 0) { + // no new items at all, we don't display anything for this channel + return; + } + + var displayedItems; + if (newItems.length > displayLimit) { + displayedItems = newItems.slice(0, displayLimit); + } else { + displayedItems = newItems; + } + var rest = newItems.length - displayedItems.length; + + var text = "

    "; + _.each(displayedItems, function(item) { + var limitedSummary = stripParagraphs(item.summary_without_images.trim()); + if (limitedSummary.length > maxLength) { + limitedSummary = limitedSummary.substr(0, maxLength); + limitedSummary = limitedSummary.substr(0, Math.min(limitedSummary.length, limitedSummary.lastIndexOf(" "))); + limitedSummary += "..."; + } + + text += "
  • " + cutAfterNewline(item.title) + "
    " + formatTimeAgo(item.published) + "

    " + limitedSummary + "

  • "; + }); + text += "
"; + + if (rest) { + text += gettext(_.sprintf("... and %(rest)d more.", {rest: rest})); + } + + var options = { + title: channel, + text: text, + hide: false, + confirm: { + confirm: true, + buttons: [{ + text: gettext("Later"), + click: function(notice) { + self.hiddenChannels.push(key); + notice.remove(); + } + }, { + text: gettext("Mark read"), + click: function(notice) { + self.markRead(key, value.last); + notice.remove(); + } + }, { + text: gettext("Read..."), + addClass: "btn-primary", + click: function(notice) { + self.showAnnouncementDialog(key); + self.markRead(key, value.last); + notice.remove(); + } + }] + }, + buttons: { + sticker: false, + closer: false + } + }; + + if (priority == 1) { + options.type = "error"; + } + + if (self.channelNotifications[key]) { + self.channelNotifications[key].remove(); + } + self.channelNotifications[key] = new PNotify(options); + }); + }; + + self.onUserLoggedIn = function() { + self.retrieveData(); + }; + + self.onStartup = function() { + self.announcementDialog = $("#plugin_announcements_dialog"); + self.announcementDialogContent = $("#plugin_announcements_dialog_content"); + self.announcementDialogTabs = $("#plugin_announcements_dialog_tabs"); + } + } + + // view model class, parameters for constructor, container to bind to + ADDITIONAL_VIEWMODELS.push([ + AnnouncementsViewModel, + ["loginStateViewModel", "settingsViewModel"], + ["#plugin_announcements_dialog", "#settings_plugin_announcements", "#navbar_plugin_announcements"] + ]); +}); diff --git a/src/octoprint/plugins/announcements/static/less/announcements.less b/src/octoprint/plugins/announcements/static/less/announcements.less new file mode 100644 index 00000000..1f57d796 --- /dev/null +++ b/src/octoprint/plugins/announcements/static/less/announcements.less @@ -0,0 +1,56 @@ +table { + th, td { + &.settings_plugin_announcements_channels_name { + text-overflow: ellipsis; + text-align: left; + } + + &.settings_plugin_announcements_channels_actions { + text-align: center; + width: 80px; + + a { + text-decoration: none; + color: #000; + + &.disabled { + color: #ccc; + cursor: default; + } + } + } + } +} + +#plugin_announcements_dialog { + .unread { + font-weight: bold; + } + + article { + padding-right: 20px; + + &.read { + opacity: 0.5; + } + + &.read:hover { + opacity: 1; + } + + .actions { + background-color: #f5f5f5; + border-radius: 2px; + padding: 2px 5px; + margin-top: 5px; + + .markread { + float: right; + } + + a { + color: black; + } + } + } +} diff --git a/src/octoprint/plugins/announcements/templates/announcements.jinja2 b/src/octoprint/plugins/announcements/templates/announcements.jinja2 new file mode 100644 index 00000000..6c565254 --- /dev/null +++ b/src/octoprint/plugins/announcements/templates/announcements.jinja2 @@ -0,0 +1,45 @@ + diff --git a/src/octoprint/plugins/announcements/templates/announcements_navbar.jinja2 b/src/octoprint/plugins/announcements/templates/announcements_navbar.jinja2 new file mode 100644 index 00000000..8f91e47e --- /dev/null +++ b/src/octoprint/plugins/announcements/templates/announcements_navbar.jinja2 @@ -0,0 +1,3 @@ + + + diff --git a/src/octoprint/plugins/announcements/templates/announcements_settings.jinja2 b/src/octoprint/plugins/announcements/templates/announcements_settings.jinja2 new file mode 100644 index 00000000..d9851abe --- /dev/null +++ b/src/octoprint/plugins/announcements/templates/announcements_settings.jinja2 @@ -0,0 +1,34 @@ +

{{ _('Configured Channels') }}

+ + + + + + + + + + + + + + +
{{ _('Name') }}{{ _('Actions') }}
+
+
 
+
+ +
+ + + diff --git a/src/octoprint/plugins/pluginmanager/templates/pluginmanager_about.jinja2 b/src/octoprint/plugins/pluginmanager/templates/pluginmanager_about.jinja2 index 6c3cd1d9..36f8fd67 100644 --- a/src/octoprint/plugins/pluginmanager/templates/pluginmanager_about.jinja2 +++ b/src/octoprint/plugins/pluginmanager/templates/pluginmanager_about.jinja2 @@ -2,7 +2,7 @@ diff --git a/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 b/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 index 0358cf3a..e0b68560 100644 --- a/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 +++ b/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 @@ -35,7 +35,7 @@
()
 
- {{ _('Homepage') }} + {{ _('Homepage') }}   diff --git a/src/octoprint/plugins/softwareupdate/templates/softwareupdate.jinja2 b/src/octoprint/plugins/softwareupdate/templates/softwareupdate.jinja2 index 2cdae112..3e1b06be 100644 --- a/src/octoprint/plugins/softwareupdate/templates/softwareupdate.jinja2 +++ b/src/octoprint/plugins/softwareupdate/templates/softwareupdate.jinja2 @@ -11,7 +11,7 @@
  • - +
  • diff --git a/src/octoprint/plugins/softwareupdate/templates/softwareupdate_settings.jinja2 b/src/octoprint/plugins/softwareupdate/templates/softwareupdate_settings.jinja2 index a36ae741..0254d765 100644 --- a/src/octoprint/plugins/softwareupdate/templates/softwareupdate_settings.jinja2 +++ b/src/octoprint/plugins/softwareupdate/templates/softwareupdate_settings.jinja2 @@ -33,7 +33,7 @@ {{ _('Installed:') }}
    {{ _('Available:') }}
    - {{ _('Release Notes:') }} + {{ _('Release Notes:') }}
    diff --git a/src/octoprint/server/__init__.py b/src/octoprint/server/__init__.py index 2a66b477..c3245236 100644 --- a/src/octoprint/server/__init__.py +++ b/src/octoprint/server/__init__.py @@ -668,9 +668,24 @@ class Server(object): return "{hashs} {content}".format(hashs="#" * number, content=match.group("content")) return markdown_header_regex.sub(repl, s) + html_link_regex = re.compile("<(?Pa.*?)>(?P.*?)") + def externalize_links(text): + def repl(match): + tag = match.group("tag") + if not u"href" in tag: + return match.group(0) + + if not u"target=" in tag and not u"rel=" in tag: + tag += u" target=\"_blank\" rel=\"noreferrer noopener\"" + + content = match.group("content") + return u"<{tag}>{content}".format(tag=tag, content=content) + return html_link_regex.sub(repl, text) + app.jinja_env.filters["regex_replace"] = regex_replace app.jinja_env.filters["offset_html_headers"] = offset_html_headers app.jinja_env.filters["offset_markdown_headers"] = offset_markdown_headers + app.jinja_env.filters["externalize_links"] = externalize_links # configure additional template folders for jinja2 import jinja2 @@ -682,7 +697,7 @@ class Server(object): loaders = [app.jinja_loader, filesystem_loader] if octoprint.util.is_running_from_source(): root = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../..")) - allowed = ["AUTHORS.md", "CHANGELOG.md", "SPONSORS.md", "THIRDPARTYLICENSES.md"] + allowed = ["AUTHORS.md", "CHANGELOG.md", "SUPPORTERS.md", "THIRDPARTYLICENSES.md"] files = {"_data/" + name: os.path.join(root, name) for name in allowed} loaders.append(octoprint.util.jinja.SelectedFilesLoader(files)) diff --git a/src/octoprint/server/views.py b/src/octoprint/server/views.py index 2e92b8af..647a302a 100644 --- a/src/octoprint/server/views.py +++ b/src/octoprint/server/views.py @@ -442,7 +442,7 @@ def _process_templates(): thirdparty=(gettext("Third Party Licenses"), dict(template="dialogs/about/thirdparty.jinja2", _div="about_thirdparty", custom_bindings=False)), authors=(gettext("Authors"), dict(template="dialogs/about/authors.jinja2", _div="about_authors", custom_bindings=False)), changelog=(gettext("Changelog"), dict(template="dialogs/about/changelog.jinja2", _div="about_changelog", custom_bindings=False)), - sponsors = (gettext("Sponsors"), dict(template="dialogs/about/sponsors.jinja2", _div="about_sponsors", custom_bindings=False)) + supporters=(gettext("Supporters"), dict(template="dialogs/about/supporters.jinja2", _div="about_sponsors", custom_bindings=False)) ) # extract data from template plugins diff --git a/src/octoprint/settings.py b/src/octoprint/settings.py index 654f6651..169fb8b3 100644 --- a/src/octoprint/settings.py +++ b/src/octoprint/settings.py @@ -214,17 +214,17 @@ default_settings = { "defaultLanguage": "_default", "components": { "order": { - "navbar": ["settings", "systemmenu", "login"], + "navbar": ["settings", "systemmenu", "login", "plugin_announcements"], "sidebar": ["connection", "state", "files"], "tab": ["temperature", "control", "gcodeviewer", "terminal", "timelapse"], "settings": [ "section_printer", "serial", "printerprofiles", "temperatures", "terminalfilters", "gcodescripts", "section_features", "features", "webcam", "accesscontrol", "api", - "section_octoprint", "server", "folders", "appearance", "logs", "plugin_pluginmanager", "plugin_softwareupdate" + "section_octoprint", "server", "folders", "appearance", "logs", "plugin_pluginmanager", "plugin_softwareupdate", "plugin_announcements" ], "usersettings": ["access", "interface"], "wizard": ["access"], - "about": ["about", "sponsors", "authors", "changelog", "license", "thirdparty", "plugin_pluginmanager"], + "about": ["about", "supporters", "authors", "changelog", "license", "thirdparty", "plugin_pluginmanager"], "generic": [] }, "disabled": { diff --git a/src/octoprint/static/js/app/viewmodels/about.js b/src/octoprint/static/js/app/viewmodels/about.js index 8ebf6b9f..73e0307e 100644 --- a/src/octoprint/static/js/app/viewmodels/about.js +++ b/src/octoprint/static/js/app/viewmodels/about.js @@ -31,6 +31,10 @@ $(function() { self.aboutContent.scrollTop(0); }); }; + + self.showTab = function(tab) { + $("a[href=#" + tab + "]", self.aboutTabs).tab("show"); + }; } OCTOPRINT_VIEWMODELS.push([ diff --git a/src/octoprint/static/js/app/viewmodels/settings.js b/src/octoprint/static/js/app/viewmodels/settings.js index ea3dcef0..87afd0e1 100644 --- a/src/octoprint/static/js/app/viewmodels/settings.js +++ b/src/octoprint/static/js/app/viewmodels/settings.js @@ -5,6 +5,7 @@ $(function() { self.loginState = parameters[0]; self.users = parameters[1]; self.printerProfiles = parameters[2]; + self.about = parameters[3]; self.allViewModels = []; @@ -799,7 +800,7 @@ $(function() { OCTOPRINT_VIEWMODELS.push([ SettingsViewModel, - ["loginStateViewModel", "usersViewModel", "printerProfilesViewModel"], + ["loginStateViewModel", "usersViewModel", "printerProfilesViewModel", "aboutViewModel"], ["#settings_dialog", "#navbar_settings"] ]); }); diff --git a/src/octoprint/static/less/octoprint.less b/src/octoprint/static/less/octoprint.less index 0e3cd9b0..47077f6e 100644 --- a/src/octoprint/static/less/octoprint.less +++ b/src/octoprint/static/less/octoprint.less @@ -694,6 +694,10 @@ ul.dropdown-menu li a { .acl_decision { margin-top: 1em; } + + .aboutlink { + float: left; + } } diff --git a/src/octoprint/templates/dialogs/about/about.jinja2 b/src/octoprint/templates/dialogs/about/about.jinja2 index 947c56ac..1569f2ca 100644 --- a/src/octoprint/templates/dialogs/about/about.jinja2 +++ b/src/octoprint/templates/dialogs/about/about.jinja2 @@ -5,17 +5,18 @@

    Version {{ display_version }}

    - OctoPrint is sponsored by a lot of awesome people. Please see "Sponsors" to the left. + Development of OctoPrint wouldn't be possible without its supporters.
    + If you enjoy OctoPrint, please consider supporting its ongoing development.

    - © 2012-{{ now.strftime("%Y") }} The OctoPrint Authors + © 2012-{{ now.strftime("%Y") }} The OctoPrint Authors

    @@ -33,16 +34,16 @@

    - For a copy of the GNU Affero General Public License, see "OctoPrint License" + For a copy of the GNU Affero General Public License, see {{ templates.about.entries.license[0] }} to the left.

    OctoPrint also utilizes various dependencies under terms of their - respective licenses, which can be found under "Third Party Licenses" to + respective licenses, which can be found under {{ templates.about.entries.thirdparty[0] }} to the left. Copyright of those dependencies lies with their respective authors

    - "OctoPrint" is a registered trademark + "OctoPrint" is a registered trademark.

    diff --git a/src/octoprint/templates/dialogs/about/authors.jinja2 b/src/octoprint/templates/dialogs/about/authors.jinja2 index 42a59fe9..93e02640 100644 --- a/src/octoprint/templates/dialogs/about/authors.jinja2 +++ b/src/octoprint/templates/dialogs/about/authors.jinja2 @@ -1 +1 @@ -{% filter markdown %}{% filter offset_markdown_headers(2) %}{% include "_data/AUTHORS.md" ignore missing %}{% endfilter %}{% endfilter %} +{% filter externalize_links %}{% filter markdown %}{% filter offset_markdown_headers(2) %}{% include "_data/AUTHORS.md" ignore missing %}{% endfilter %}{% endfilter %}{% endfilter %} diff --git a/src/octoprint/templates/dialogs/about/changelog.jinja2 b/src/octoprint/templates/dialogs/about/changelog.jinja2 index 60c34b3a..672f2cba 100644 --- a/src/octoprint/templates/dialogs/about/changelog.jinja2 +++ b/src/octoprint/templates/dialogs/about/changelog.jinja2 @@ -1 +1 @@ -{% filter markdown %}{% filter offset_markdown_headers(2) %}{% include "_data/CHANGELOG.md" ignore missing %}{% endfilter %}{% endfilter %} +{% filter externalize_links %}{% filter markdown %}{% filter offset_markdown_headers(2) %}{% include "_data/CHANGELOG.md" ignore missing %}{% endfilter %}{% endfilter %}{% endfilter %} diff --git a/src/octoprint/templates/dialogs/about/license.jinja2 b/src/octoprint/templates/dialogs/about/license.jinja2 index 38848981..5ef3980c 100644 --- a/src/octoprint/templates/dialogs/about/license.jinja2 +++ b/src/octoprint/templates/dialogs/about/license.jinja2 @@ -1 +1 @@ -{% include "_data/agpl.html" %} +{% filter externalize_links %}{% include "_data/agpl.html" %}{% endfilter %} diff --git a/src/octoprint/templates/dialogs/about/sponsors.jinja2 b/src/octoprint/templates/dialogs/about/sponsors.jinja2 deleted file mode 100644 index 1870c618..00000000 --- a/src/octoprint/templates/dialogs/about/sponsors.jinja2 +++ /dev/null @@ -1 +0,0 @@ -{% filter markdown %}{% filter offset_markdown_headers(2) %}{% include "_data/SPONSORS.md" ignore missing %}{% endfilter %}{% endfilter %} diff --git a/src/octoprint/templates/dialogs/about/supporters.jinja2 b/src/octoprint/templates/dialogs/about/supporters.jinja2 new file mode 100644 index 00000000..d8bb5b3f --- /dev/null +++ b/src/octoprint/templates/dialogs/about/supporters.jinja2 @@ -0,0 +1 @@ +{% filter externalize_links %}{% filter markdown %}{% filter offset_markdown_headers(2) %}{% include "_data/SUPPORTERS.md" ignore missing %}{% endfilter %}{% endfilter %}{% endfilter %} diff --git a/src/octoprint/templates/dialogs/about/thirdparty.jinja2 b/src/octoprint/templates/dialogs/about/thirdparty.jinja2 index ab4e48e4..1ea4b492 100644 --- a/src/octoprint/templates/dialogs/about/thirdparty.jinja2 +++ b/src/octoprint/templates/dialogs/about/thirdparty.jinja2 @@ -1 +1 @@ -{% filter markdown %}{% filter offset_markdown_headers(2) %}{% include "_data/THIRDPARTYLICENSES.md" ignore missing %}{% endfilter %}{% endfilter %} +{% filter externalize_links %}{% filter markdown %}{% filter offset_markdown_headers(2) %}{% include "_data/THIRDPARTYLICENSES.md" ignore missing %}{% endfilter %}{% endfilter %}{% endfilter %} diff --git a/src/octoprint/templates/dialogs/settings.jinja2 b/src/octoprint/templates/dialogs/settings.jinja2 index c478fc6b..0a5b0fef 100644 --- a/src/octoprint/templates/dialogs/settings.jinja2 +++ b/src/octoprint/templates/dialogs/settings.jinja2 @@ -48,6 +48,7 @@
    diff --git a/src/octoprint/templates/index.jinja2 b/src/octoprint/templates/index.jinja2 index cbd644c0..60ad8bca 100644 --- a/src/octoprint/templates/index.jinja2 +++ b/src/octoprint/templates/index.jinja2 @@ -115,10 +115,10 @@
  • {{ _('Version') }}: {{ display_version|e }}