Merge branch 'maintenance' into devel

Conflicts:
	CHANGELOG.md
This commit is contained in:
Gina Häußge 2016-05-09 10:29:20 +02:00
commit 3322714a8a
13 changed files with 1195 additions and 859 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -97,6 +97,10 @@ $(function() {
})
};
self.refreshAnnouncements = function() {
self.retrieveData(true);
};
self.retrieveData = function(force) {
if (!self.loginState.isAdmin()) return;

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff