Merge branch 'maintenance' into devel
Conflicts: CHANGELOG.md
This commit is contained in:
commit
3322714a8a
13 changed files with 1195 additions and 859 deletions
|
|
@ -14,11 +14,11 @@ master
|
|||
HEAD
|
||||
\(detached.*
|
||||
|
||||
# maintenance is currently the branch for preparation of maintenance release 1.2.11
|
||||
# maintenance is currently the branch for preparation of maintenance release 1.2.12
|
||||
# so are any fix/... and improve/... branches
|
||||
maintenance 1.2.11 692166f067329cd3d6fdc84389e0dd76184c5e0c pep440-dev
|
||||
fix/.* 1.2.11 692166f067329cd3d6fdc84389e0dd76184c5e0c pep440-dev
|
||||
improve/.* 1.2.11 692166f067329cd3d6fdc84389e0dd76184c5e0c pep440-dev
|
||||
maintenance 1.2.12 e79703ba6eec1ecdfa21cf8f2a9efbc1eb6120e3 pep440-dev
|
||||
fix/.* 1.2.12 e79703ba6eec1ecdfa21cf8f2a9efbc1eb6120e3 pep440-dev
|
||||
improve/.* 1.2.12 e79703ba6eec1ecdfa21cf8f2a9efbc1eb6120e3 pep440-dev
|
||||
|
||||
# every other branch is a development branch and thus gets resolved to 1.3.0-dev for now
|
||||
.* 1.3.0 198d3450d94be1a2 pep440-dev
|
||||
|
|
|
|||
22
CHANGELOG.md
22
CHANGELOG.md
|
|
@ -69,6 +69,28 @@
|
|||
* [#1047](https://github.com/foosel/OctoPrint/issues/1047) - Fixed 90 degree
|
||||
webcam rotation for iOS Safari.
|
||||
|
||||
## 1.2.11 (2016-05-04)
|
||||
|
||||
### Important Announcement
|
||||
|
||||
Due to a recent change in the financial situation of the project, the funding of OctoPrint is at stake. If you love OctoPrint and want to see its development continue at the pace of the past two years, please read on about its current funding situation and how you can help: ["I need your support"](http://octoprint.org/blog/2016/04/13/i-need-your-support/).
|
||||
|
||||
### Improvements
|
||||
|
||||
* Added option to treat resend requests as `ok` for such firmwares that do not send an `ok` after requesting a resend. If you printer communication gets stalled after a resend request from the firmware, try checking this option.
|
||||
* Added an "About" dialog to properly inform about OctoPrint's license, contributors and supporters.
|
||||
* Added a announcement plugin that utilizes the RSS feeds of the [OctoPrint Blog](http://octoprint.org/blog/) and the [plugin repository](http://plugins.octoprint.org) to display news to the user. By default only the "important announcement" category is enabled. This category will only be used for very rare situations such as making you aware of critical updates or important news. You can enable further categories (with more announcements to be expected) in the plugin's settings dialog.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#1300](https://github.com/foosel/OctoPrint/issues/1300) - Removed possibility to accidentally disabling local file list by first limiting view to files from SD and then disabling SD support.
|
||||
* [#1315](https://github.com/foosel/OctoPrint/issues/1315) - Fixed broken post roll on z-based timelapses.
|
||||
* Fixed CSS data binding syntax on the download link in the files list
|
||||
* Changed control distance from jQuery data into a knockout observerable and observerableArray
|
||||
* Allow an unauthorized user to logout from a logedin interface state
|
||||
|
||||
([Commits](https://github.com/foosel/OctoPrint/compare/1.2.10...1.2.11))
|
||||
|
||||
## 1.2.10 (2016-03-16)
|
||||
|
||||
### Improvements
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Supporters
|
||||
# Supporters
|
||||
|
||||
Development of this version of OctoPrint wouldn't have been possible without
|
||||
[financial support by the community](http://octoprint.org/support-octoprint/) -
|
||||
|
|
@ -7,25 +7,35 @@ thanks to everyone who contributed!
|
|||
## Patreon Patrons
|
||||
|
||||
* 3D Moniak
|
||||
* Andrew Moorby
|
||||
* Arnljot Arntsen
|
||||
* Aurelio Bernal Ramírez
|
||||
* Brian E. Tyler
|
||||
* Christian Petropolis
|
||||
* COLLE+McVOY
|
||||
* CreativeTools
|
||||
* D Brian Kimmel
|
||||
* Doug Johnson
|
||||
* E3D BigBox
|
||||
* Erik de Bruijn
|
||||
* Ernesto Martinez
|
||||
* Exovite
|
||||
* georgeroblesjr
|
||||
* Exovite
|
||||
* georgeroblesjr
|
||||
* Gregor Luetolf
|
||||
* Joshua Gregory
|
||||
* Kale Stedman
|
||||
* Kyle Gress
|
||||
* Makespace Madrid
|
||||
* Masayoshi Mitsui
|
||||
* Miguel Angel Salmeron
|
||||
* MikeyDK
|
||||
* Mohammed Khorakiwala
|
||||
* Noe Ruiz
|
||||
* Roy Cortes
|
||||
* Samer Najia
|
||||
* Stefan Krister
|
||||
* Steven Pearson
|
||||
* Sven Mueller
|
||||
* Tom
|
||||
* Tom
|
||||
|
||||
and 321 more wonderful people pledging on the [Patreon campaign](https://patreon.com/foosel)!
|
||||
and 414 more wonderful people pledging on the [Patreon campaign](https://patreon.com/foosel)!
|
||||
|
|
@ -29,8 +29,12 @@ class AnnouncementPlugin(octoprint.plugin.AssetPlugin,
|
|||
octoprint.plugin.TemplatePlugin):
|
||||
|
||||
def __init__(self):
|
||||
self._cached_channels = dict()
|
||||
self._cached_channels_mutex = threading.RLock()
|
||||
self._cached_channel_configs = None
|
||||
self._cached_channel_configs_mutex = threading.RLock()
|
||||
|
||||
from slugify import Slugify
|
||||
self._slugify = Slugify()
|
||||
self._slugify.safe_chars = "-_."
|
||||
|
||||
# StartupPlugin
|
||||
|
||||
|
|
@ -48,23 +52,19 @@ class AnnouncementPlugin(octoprint.plugin.AssetPlugin,
|
|||
_releases=dict(name="OctoPrint Release Announcements",
|
||||
priority=2,
|
||||
type="rss",
|
||||
url="http://octoprint.org/feeds/releases.xml",
|
||||
read_until=1458121176),
|
||||
url="http://octoprint.org/feeds/releases.xml"),
|
||||
_spotlight=dict(name="OctoPrint Community Spotlights",
|
||||
priority=2,
|
||||
type="rss",
|
||||
url="http://octoprint.org/feeds/spotlight.xml",
|
||||
read_until=1447953971),
|
||||
url="http://octoprint.org/feeds/spotlight.xml"),
|
||||
_octopi=dict(name="OctoPi Announcements",
|
||||
priority=2,
|
||||
type="rss",
|
||||
url="http://octoprint.org/feeds/octopi.xml",
|
||||
read_until=1462200600),
|
||||
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",
|
||||
read_until=1461628800)),
|
||||
url="http://plugins.octoprint.org/feed.xml")),
|
||||
enabled_channels=[],
|
||||
forced_channels=["_important"],
|
||||
ttl=6*60,
|
||||
|
|
@ -92,20 +92,30 @@ class AnnouncementPlugin(octoprint.plugin.AssetPlugin,
|
|||
@restricted_access
|
||||
@admin_permission.require(403)
|
||||
def get_channel_data(self):
|
||||
from octoprint.settings import valid_boolean_trues
|
||||
|
||||
result = dict()
|
||||
|
||||
channel_data = self._fetch_all_channels()
|
||||
force = "force" in flask.request.values and flask.request.values["force"] in valid_boolean_trues
|
||||
|
||||
channel_data = self._fetch_all_channels(force=force)
|
||||
channel_configs = self._get_channel_configs(force=force)
|
||||
|
||||
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))
|
||||
read_until = channel_configs[key].get("read_until", None)
|
||||
entries = sorted(self._to_internal_feed(channel_data.get(key, []), read_until=read_until), key=lambda e: e["published"], reverse=True)
|
||||
unread = len(filter(lambda e: not e["read"], entries))
|
||||
|
||||
if read_until is None and entries:
|
||||
last = entries[0]["published"]
|
||||
self._mark_read_until(key, last)
|
||||
|
||||
result[key] = dict(channel=data["name"],
|
||||
url=data["url"],
|
||||
priority=data["priority"],
|
||||
priority=data.get("priority", 2),
|
||||
enabled=key in enabled or key in forced,
|
||||
forced=key in forced,
|
||||
data=entries,
|
||||
|
|
@ -128,63 +138,106 @@ class AnnouncementPlugin(octoprint.plugin.AssetPlugin,
|
|||
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()
|
||||
self._mark_read_until(channel, until)
|
||||
|
||||
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()
|
||||
self._toggle(channel)
|
||||
|
||||
return NO_CONTENT
|
||||
|
||||
# Internal Tools
|
||||
|
||||
def _get_channel_configs(self):
|
||||
return self._settings.get(["channels"], merged=True)
|
||||
def _mark_read_until(self, channel, until):
|
||||
"""Set read_until timestamp of a channel."""
|
||||
|
||||
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"])
|
||||
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)
|
||||
|
||||
all_channels = dict()
|
||||
for key, config in channels.items():
|
||||
if not key in enabled and not key in forced:
|
||||
continue
|
||||
defaults = dict(plugins=dict(announcements=dict(channels=dict())))
|
||||
defaults["plugins"]["announcements"]["channels"][channel] = dict(read_until=current_read_until)
|
||||
|
||||
data = self._get_channel_data(key, config)
|
||||
if data is not None:
|
||||
all_channels[key] = data
|
||||
with self._cached_channel_configs_mutex:
|
||||
self._settings.set(["channels", channel, "read_until"], until, defaults=defaults)
|
||||
self._settings.save()
|
||||
self._cached_channel_configs = None
|
||||
|
||||
self._cached_channels = all_channels
|
||||
def _toggle(self, channel):
|
||||
"""Toggle enable/disabled state of a channel."""
|
||||
|
||||
return self._cached_channels
|
||||
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()
|
||||
|
||||
def _get_channel_configs(self, force=False):
|
||||
"""Retrieve all channel configs with sanitized keys."""
|
||||
|
||||
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:
|
||||
# strip invalid entries
|
||||
continue
|
||||
result[self._slugify(key)] = config
|
||||
self._cached_channel_configs = result
|
||||
return self._cached_channel_configs
|
||||
|
||||
def _get_channel_config(self, key, force=False):
|
||||
"""Retrieve specific channel config for channel."""
|
||||
|
||||
safe_key = self._slugify(key)
|
||||
return self._get_channel_configs(force=force).get(safe_key)
|
||||
|
||||
def _fetch_all_channels(self, force=False):
|
||||
"""Fetch all channel feeds from cache or network."""
|
||||
|
||||
channels = self._get_channel_configs(force=force)
|
||||
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
|
||||
|
||||
if not "url" in config:
|
||||
continue
|
||||
|
||||
data = self._get_channel_data(key, config, force=force)
|
||||
if data is not None:
|
||||
all_channels[key] = data
|
||||
|
||||
return all_channels
|
||||
|
||||
def _get_channel_data(self, key, config, force=False):
|
||||
"""Fetch individual channel feed from cache/network."""
|
||||
|
||||
data = None
|
||||
|
||||
if not force:
|
||||
# we may use the cache, see if we have something in there
|
||||
data = self._get_channel_data_from_cache(key, config)
|
||||
|
||||
def _get_channel_data(self, key, config):
|
||||
data = self._get_channel_data_from_cache(key, config)
|
||||
if data is None:
|
||||
# cache not allowed or empty, fetch from network
|
||||
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))
|
||||
"""Fetch channel feed from cache."""
|
||||
|
||||
channel_path = self._get_channel_cache_path(key)
|
||||
|
||||
if os.path.exists(channel_path):
|
||||
if "ttl" in config and isinstance(config["ttl"], int):
|
||||
|
|
@ -196,30 +249,35 @@ class AnnouncementPlugin(octoprint.plugin.AssetPlugin,
|
|||
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))
|
||||
self._logger.debug(u"Loaded channel {} from cache at {}".format(key, channel_path))
|
||||
return d
|
||||
|
||||
return None
|
||||
|
||||
def _get_channel_data_from_network(self, key, config):
|
||||
"""Fetch channel feed from network."""
|
||||
|
||||
import requests
|
||||
|
||||
url = config["url"]
|
||||
try:
|
||||
start = time.time()
|
||||
r = requests.get(url)
|
||||
self._logger.info("Loaded channel {} from {}".format(key, config["url"]))
|
||||
self._logger.info(u"Loaded channel {} from {} in {:.2}s".format(key, config["url"], time.time() - start))
|
||||
except Exception as e:
|
||||
self._logger.exception(
|
||||
"Could not fetch channel {} from {}: {}".format(key, config["url"], str(e)))
|
||||
u"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))
|
||||
channel_path = self._get_channel_cache_path(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):
|
||||
"""Convert feed to internal data structure."""
|
||||
|
||||
result = []
|
||||
if "entries" in feed:
|
||||
for entry in feed["entries"]:
|
||||
|
|
@ -229,9 +287,11 @@ class AnnouncementPlugin(octoprint.plugin.AssetPlugin,
|
|||
return result
|
||||
|
||||
def _to_internal_entry(self, entry, read_until=None):
|
||||
"""Convert feed entries to internal data structure."""
|
||||
|
||||
published = calendar.timegm(entry["published_parsed"])
|
||||
|
||||
read = False
|
||||
read = True
|
||||
if read_until is not None:
|
||||
read = published <= read_until
|
||||
|
||||
|
|
@ -243,6 +303,12 @@ class AnnouncementPlugin(octoprint.plugin.AssetPlugin,
|
|||
link=entry["link"],
|
||||
read=read)
|
||||
|
||||
def _get_channel_cache_path(self, key):
|
||||
"""Retrieve cache path for channel key."""
|
||||
|
||||
safe_key = self._slugify(key)
|
||||
return os.path.join(self.get_plugin_data_folder(), "{}.cache".format(safe_key))
|
||||
|
||||
|
||||
_image_tag_re = re.compile(r'<img.*?/?>')
|
||||
def _strip_images(text):
|
||||
|
|
|
|||
|
|
@ -97,6 +97,10 @@ $(function() {
|
|||
})
|
||||
};
|
||||
|
||||
self.refreshAnnouncements = function() {
|
||||
self.retrieveData(true);
|
||||
};
|
||||
|
||||
self.retrieveData = function(force) {
|
||||
if (!self.loginState.isAdmin()) return;
|
||||
|
||||
|
|
|
|||
|
|
@ -31,4 +31,4 @@
|
|||
</ul>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-block" data-bind="click: $root.showAnnouncementDialog">{{ _('Show Announcements...') }}</button>
|
||||
<button class="btn btn-block" data-bind="click: $root.refreshAnnouncements"><i class="icon-refresh"></i> {{ _('Refresh Announcements') }}</button>
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
|
|||
def get_template_configs(self):
|
||||
return [
|
||||
dict(type="settings", name=gettext("Plugin Manager"), template="pluginmanager_settings.jinja2", custom_bindings=True),
|
||||
dict(type="about", name=gettext("Plugin Licenses"), template="pluginmanager_about.jinja2")
|
||||
dict(type="about", name="Plugin Licenses", template="pluginmanager_about.jinja2")
|
||||
]
|
||||
|
||||
def get_template_vars(self):
|
||||
|
|
|
|||
|
|
@ -437,12 +437,12 @@ def _process_templates():
|
|||
# about dialog
|
||||
|
||||
templates["about"]["entries"] = dict(
|
||||
about=(gettext("About OctoPrint"), dict(template="dialogs/about/about.jinja2", _div="about_about", custom_bindings=False)),
|
||||
license=(gettext("OctoPrint License"), dict(template="dialogs/about/license.jinja2", _div="about_license", custom_bindings=False)),
|
||||
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)),
|
||||
supporters=(gettext("Supporters"), dict(template="dialogs/about/supporters.jinja2", _div="about_sponsors", custom_bindings=False))
|
||||
about=("About OctoPrint", dict(template="dialogs/about/about.jinja2", _div="about_about", custom_bindings=False)),
|
||||
license=("OctoPrint License", dict(template="dialogs/about/license.jinja2", _div="about_license", custom_bindings=False)),
|
||||
thirdparty=("Third Party Licenses", dict(template="dialogs/about/thirdparty.jinja2", _div="about_thirdparty", custom_bindings=False)),
|
||||
authors=("Authors", dict(template="dialogs/about/authors.jinja2", _div="about_authors", custom_bindings=False)),
|
||||
changelog=("Changelog", dict(template="dialogs/about/changelog.jinja2", _div="about_changelog", custom_bindings=False)),
|
||||
supporters=("Supporters", dict(template="dialogs/about/supporters.jinja2", _div="about_sponsors", custom_bindings=False))
|
||||
)
|
||||
|
||||
# extract data from template plugins
|
||||
|
|
|
|||
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue