diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 229f4b4a62..97a7e30ccb 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,31 +1,34 @@ READ THE FOLLOWING FIRST: If not already done, please read the "guidelines for contributing" -that are linked ^-- just up there in the big yellow box. Also read -the FAQ: https://github.com/foosel/OctoPrint/wiki/FAQ. +aka the Contribution Guidelines that are linked ^-- just up there +in the big yellow box. + +Also read the FAQ: https://github.com/foosel/OctoPrint/wiki/FAQ. This is a bug and feature tracker, please only use it to report bugs or request features within OctoPrint (not OctoPi, not any OctoPrint plugins and not unofficial OctoPrint versions). -Do not seek support here ("I need help with ..."), that belongs on -the mailing list or the G+ community (both linked in the "guidelines -for contributing" linked above, read it!), NOT here. +Do not seek support here ("I need help with ...", "I have a +question ..."), that belongs on the mailing list or the G+ community +(both linked in the "guidelines for contributing" linked above, read +them!), NOT here. -Mark requests with a "[Request]" prefix in the title please. Fully fill -out the bug reporting template for bug reports (if you don't know where -to find some information - it's all described in the contribution -guidelines linked up there in the big yellow box). +Mark requests with a "[Request]" prefix in the title please. For bug +reports fully fill out the bug reporting template (if you don't know +where to find some information - it's all described in the Contribution +Guidelines linked up there in the big yellow box). -When reporting a bug do NOT delete any lines from the template but +When reporting a bug do NOT delete ANY lines from the template but those enclosed in [ and ] - and those please DO delete, they are only provided for your information and removing them makes your ticket more readable :) -Make sure any bug you want to repor tis still present with the CURRENT +Make sure any bug you want to report is still present with the CURRENT OctoPrint version and that it does not vanish when you start OctoPrint -in safe mode - how to do that is also explained in the contribution -guidelines linked up there in the big yellow box. +in safe mode - how to do that is also explained in the Contribution +Guidelines linked up there in the big yellow box. Thank you! @@ -37,15 +40,27 @@ by heart ;)) #### What were you doing? -[Please describe the steps to reproduce your issue. Be as specific as -possible here. The maintainers will need to reproduce your issue in -order to fix it and that is not possible if they don't know what you -did to get it to happen in the first place. +[Please be as specific as possible here. The maintainers will need to +reproduce your issue in order to fix it and that is not possible if they +don't know what you did to get it to happen in the first place. + +Ideally provide exact steps to follow in order to reproduce your problem: + +1. ... +2. ... +3. ... If you encountered a problem with specific files of any sorts, make sure to also include a link to a file with which to reproduce the problem.] -#### What did you expect to happen and what happened instead? +#### What did you expect to happen? + +#### What happened instead? + +#### Did the same happen when running OctoPrint in safe mode? + +[Try to reproduce your problem in safe mode. You can find information +on how to enable safe mode in the Contribution Guidelines.] #### Branch & Commit or Version of OctoPrint @@ -53,7 +68,8 @@ to also include a link to a file with which to reproduce the problem.] #### Operating System running OctoPrint -[OctoPi, Linux, Windows, MacOS, something else? With version please.] +[OctoPi, Linux, Windows, MacOS, something else? With version please. +OctoPi's version can be found in /etc/octopi_version] #### Printer model & used firmware incl. version @@ -61,29 +77,31 @@ to also include a link to a file with which to reproduce the problem.] #### Browser and Version of Browser, Operating System running Browser -[If applicable, always include if unsure or reporting UI issues.] +[If applicable, always include if unsure.] #### Link to octoprint.log -[On gist.github.com or pastebin.com. ALWAYS INCLUDE and never truncate.] +[On gist.github.com or pastebin.com. ALWAYS INCLUDE and never truncate. +The Contribution Guidelines tell you where to find that.] #### Link to contents of terminal tab or serial.log [On gist.github.com or pastebin.com. If applicable, always include if unsure or -reporting any kind of communication issues between OctoPrint and your printer. -Never truncate. +reporting communication issues. Never truncate. serial.log is usually not written due to performance reasons and must be enabled explicitly. Provide at the very least the FULL contents of your terminal tab at the time of the bug occurrence, even if you do not have -a serial.log.] +a serial.log (which the Contribution Guidelines tell you where to find).] #### Link to contents of Javascript console in the browser [On gist.github.com or pastebin.com or alternatively a screenshot. If applicable - -always include if unsure or reporting UI issues.] +always include if unsure or reporting UI issues. + +The Contribution Guidelines tell you where to find that.] -#### Screenshot(s) or video(s) showing the problem: +#### Screenshot(s)/video(s) showing the problem: [If applicable. Always include if unsure or reporting UI issues.] diff --git a/.versioneer-lookup b/.versioneer-lookup index 9f0f950c29..e52acd1d9d 100644 --- a/.versioneer-lookup +++ b/.versioneer-lookup @@ -18,11 +18,14 @@ prerelease HEAD \(detached.* -# maintenance is currently the branch for preparation of maintenance release 1.3.2 +# maintenance is currently the branch for preparation of maintenance release 1.3.3 # so are any fix/... and improve/... branches -maintenance 1.3.2 6393de8c7d42a8bbddcab7cdbb6530ea88a8c82d pep440-dev -fix/.* 1.3.2 6393de8c7d42a8bbddcab7cdbb6530ea88a8c82d pep440-dev -improve/.* 1.3.2 6393de8c7d42a8bbddcab7cdbb6530ea88a8c82d pep440-dev +maintenance 1.3.3 0a69dbeddb301d5a32827a3f0d561f875df24234 pep440-dev +fix/.* 1.3.3 0a69dbeddb301d5a32827a3f0d561f875df24234 pep440-dev +improve/.* 1.3.3 0a69dbeddb301d5a32827a3f0d561f875df24234 pep440-dev + +# staging/maintenance is currently the branch for preparation of 1.3.3rc4 (if we'll need that) +staging/maintenance 1.3.3rc4 ce1541e956778b732458599a21f38b6783a0c2ec pep440-dev # every other branch is a development branch and thus gets resolved to 1.4.0-dev for now .* 1.4.0 7f5d03d0549bcbd26f40e7e4a3297ea5204fb1cc pep440-dev diff --git a/AUTHORS.md b/AUTHORS.md index 4279200cfa..bea57d2ed6 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -79,6 +79,7 @@ date of first contribution): * [Greg Hulands](https://github.com/ghulands) * [Andreas Werner](https://github.com/gallore) * [Shawn Bruce](https://github.com/kantlivelong) + * [Claudiu Ceia] (https://github.com/ClaudiuCeia) OctoPrint started off as a fork of [Cura](https://github.com/daid/Cura) by [Daid Braam](https://github.com/daid). Parts of its communication layer and diff --git a/CHANGELOG.md b/CHANGELOG.md index 07abd8dbaa..3a36cf58f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,124 @@ # OctoPrint Changelog +## 1.3.4 (2017-06-01) + +### Note for owners of Malyan M200/Monoprice Select Mini + +OctoPrint's firmware autodetection is now able to detect this printer. Currently when this printer is detected, the following firmware specific features will be enabled automatically: + + * Always assume SD card is present (`feature.sdAlwaysAvailable`) + * Send a checksum with the command: Always (`feature.alwaysSendChecksum`) + +Since the firmware is a very special kind of beast and its sources are so far unavailable, only tests with a real printer will show if those are sufficient settings for communication with this printer to fully function correctly. Thus, if you run into any issues with enabled firmware autodetection on this printer model, please add a comment in [#1762](https://github.com/foosel/OctoPrint/issues/1762) and explain what kind of communication problem you are seeing. Also make sure to include a [`serial.log`](https://github.com/foosel/OctoPrint/blob/master/CONTRIBUTING.md#where-can-i-find-those-log-files-you-keep-talking-about)! + +### Bug fixes + + * [#1942](https://github.com/foosel/OctoPrint/issues/1942) - Fixed crash on startup in case of an invalid default printer profile combined with "auto-connect on startup" being selected and the printer available to connect to. + +### More information + + * [Commits](https://github.com/foosel/OctoPrint/compare/1.3.1...1.3.2) + * Release Candidates: + * None since this constitutes a hotfix release to fix an apparently very rare bug introduced with 1.3.3 that seems to be affecting a small number of users. + +## 1.3.3 (2017-05-31) + +### Note for owners of Malyan M200/Monoprice Select Mini + +OctoPrint's firmware autodetection is now able to detect this printer. Currently when this printer is detected, the following firmware specific features will be enabled automatically: + + * Always assume SD card is present (`feature.sdAlwaysAvailable`) + * Send a checksum with the command: Always (`feature.alwaysSendChecksum`) + +Since the firmware is a very special kind of beast and its sources are so far unavailable, only tests with a real printer will show if those are sufficient settings for communication with this printer to fully function correctly. Thus, if you run into any issues with enabled firmware autodetection on this printer model, please add a comment in [#1762](https://github.com/foosel/OctoPrint/issues/1762) and explain what kind of communication problem you are seeing. Also make sure to include a [`serial.log`](https://github.com/foosel/OctoPrint/blob/master/CONTRIBUTING.md#where-can-i-find-those-log-files-you-keep-talking-about)! + +### Improvements + + * [#478](https://github.com/foosel/OctoPrint/issues/478) - Made webcam stream container fixed height (with selectable aspect ratio) to prevent jumps of the controls beneath it on load. + * [#748](https://github.com/foosel/OctoPrint/issues/748) - Added delete confirmation and bulk delete for timelapses. See also the discussion in brainstorming ticket [#1807](https://github.com/foosel/OctoPrint/issues/1807). + * [#1092](https://github.com/foosel/OctoPrint/issues/1092) - Added new events to the file manager: `FileAdded`, `FileRemoved`, `FolderAdded`, `FolderRemoved`. Contrary to the `Upload` event, `FileAdded` will always fire when a file was added to storage through the file manager, not only when added through the web interface. Extended documentation accordingly. + * [#1521](https://github.com/foosel/OctoPrint/issues/1521) - Software update plugin: Display timestamp of last version cache refresh in "Advanced options" area. + * [#1734](https://github.com/foosel/OctoPrint/issues/1734) - Treat default/initial printer profile like all other printer profiles, persisting it to disk instead of `config.yaml` and allowing deletion. OctoPrint will migrate the existing default profile to the new location on first start. + * [#1734](https://github.com/foosel/OctoPrint/issues/1734) - Better communication of what actions are available for printer profiles. + * [#1739](https://github.com/foosel/OctoPrint/issues/1739) - Software update plugin: Added option to hide update notification from users without admin rights, added "ignore" button and note to get in touch with an admit to update notifications for non admin users. + * [#1762](https://github.com/foosel/OctoPrint/issues/1762) - Added Malyan M200/Monoprice Select Mini to firmware autodetection. + * [#1811](https://github.com/foosel/OctoPrint/issues/1811) - Slight rewording and rearrangement in timelapse configuration, better feedback if settings have been saved. + * [#1818](https://github.com/foosel/OctoPrint/issues/1818) - Support both Marlin/Repetier and Smoothieware interpretations of `G90` after an `M83` in GCODE viewer and analysis. Select "G90/G91 overrides relative extruder mode" in Settings > Features for the Smoothieware interpretation. + * [#1858](https://github.com/foosel/OctoPrint/issues/1858) - Announcement plugin: Images from registered feeds now are lazy loading. + * [#1862](https://github.com/foosel/OctoPrint/issues/1862) - Automatically re-enable fancy terminal functionality when performance recovers. + * [#1875](https://github.com/foosel/OctoPrint/issues/1875) - Marked the command input field in the Terminal tab as not supporting autocomplete to work around an issue in Safari. Note that this is probably only a temporary workaround due to browser vendors [working on deprecating `autocomplete="off"` support](https://bugs.chromium.org/p/chromium/issues/detail?id=468153#c164) and a different solution will need to be found in the future. + * Added link to [`SerialException` FAQ entry](https://github.com/foosel/OctoPrint/wiki/FAQ#octoprint-randomly-loses-connection-to-the-printer-with-a-serialexception) to terminal output when such an error is encountered, as suggested in [#1876](https://github.com/foosel/OctoPrint/issues/1876). + * Force refresh of settings on login/logout. + * Made system wide API key management mirror user API key management. + * Make sure to always migrate and merge saved printer profiles with default profile to ensure all parameters are set. Should avoid issues with plugins trying to save outdated/incomplete profiles. + * Added note on lack of language pack repository & to use the wiki for now. + * Earlier validation of file to select for printing. + * Limit verbosity of failed system event handlers. + * Made bundled python client `octoprint_client` support multiple client instances. + * Disable "Reload" button in the "Please reload" overlay once clicked, added spinner. + * Updated pnotify to 2.1.0. + * Get rid of ridiculous float precision in temperature commands. + * Detect invalid settings data to persist (not a dict), send 400 status in such a case. + * More logging for preemptive caching, to help narrow down any performance issues that might be related to this. + * Further decoupling of some startup tasks from initial server startup thread for better parallelization and improved startup times. + * Announcement plugin: Added combined OctoBlog feed, replacing news and spotlight feed, added corresponding config migration. + * Announcement plugin: Subscribe to all registered feeds by default to ensure better communication flow (all subscriptions but the "Important" channel can however be unsubscribed easily, added corresponding note to the notifications and also a configuration button to the announcement reader). + * Announcement plugin: Auto-hide announcements on logout. + * Announcement plugin: Order channels server-side based on new order config setting. + * Plugin manager: Show warning when disabling a bundled plugin that is not recommended to be disabled, including a reason why disabling it is not recommended. Applies to the bundled announcement, core wizard, discovery and software update plugins. + * Plugin manager: Support for plugin notices for specific plugins from the plugin repository, e.g. to inform users of specific plugins about known issues with the plugin or instruct to update when the software update mechanism of the current plugin version turns out to be misconfigured. Supports matching installed plugin versions and OctoPrint versions to target only affected users. + * Plugin manager: Better visualization of plugins disabled on the repository, no longer shown as "incompatible" but "disabled", with link to the plugin repository page that contains more information. + * Plugin manager: Detect necessity to reinstall a plugin provided through archive URL or upload and immediately do that instead of reporting an "unknown error" without further information. + * Plugin manager: Added `freebsd` for compatibility check. + * Plugin manager: More general flexibility for OS compatibility check: + * Support for arbitrary values to match against + * Allow 1:1 check again `sys.platform` values (with `startswith`). + * Support black listing (`!windows`) additionally to white listing. A detected OS must match all provided white list elements (if the white list is empty that is considered to be always the case) and none of the black list elements (if the black list is empty that is also considered to be always the case). + * Software update plugin: New check type `bitbucket_commit` (see also [#1898](https://github.com/foosel/OctoPrint/pull/1898)) + * Docs: Now referring to dedicated Jinja 2.8 documentation as hosted at [jinja.octoprint.org](http://jinja.octoprint.org) for all template needs, to avoid confusion when consulting current Jinja documentation as available on its project page (2.9+, which OctoPrint can't upgrade to due to backwards incompatible changes). + * Docs: Better documentation of what kind of input the `FileManager` accepts for `select_file`. + * Docs: Specified OctoPrint version required for plugin tutorial. + +### Bug fixes + + * [#202](https://github.com/foosel/OctoPrint/issues/202) - Fixed an issue with the drag-n-drop area flickering if the mouse was moved too slow while dragging (see also [#1867](https://github.com/foosel/OctoPrint/pull/1867)). + * [#1671](https://github.com/foosel/OctoPrint/issues/1671) - Removed obsolete entry of no longer available filter for empty folders from file list options. + * [#1821](https://github.com/foosel/OctoPrint/issues/1821) - Properly reset "Capture post roll images" setting in timelapse configuration when switching from "off" to "timed" timelapse mode. + * [#1822](https://github.com/foosel/OctoPrint/issues/1822) - Properly reset file metadata when a file is overwritten with a new version. + * [#1836](https://github.com/foosel/OctoPrint/issues/1836) - Fixed order of `PrintCancelled` and `PrintFailed` events on print cancel. + * [#1837](https://github.com/foosel/OctoPrint/issues/1837) - Fixed a race condition causing OctoPrint trying to read data from the current job on job cancel that was no longer there. + * [#1838](https://github.com/foosel/OctoPrint/issues/1838) - Fixed a rare race condition causing an error right at the very start of a print. + * [#1863](https://github.com/foosel/OctoPrint/issues/1863) - Fixed an issue in the analysis of GCODE files containing coordinate offsets for X, Y or Z via `G92`, leading to a wrong calculation of the model size thanks to accumulating offsets. + * [#1882](https://github.com/foosel/OctoPrint/issues/1882) - Fixed a rare race condition occurring at the start of streaming a file to the printer's SD card, leading to endless line number mismatches. + * [#1884](https://github.com/foosel/OctoPrint/issues/1884) - CuraEngine plugin: Fixed a potential encoding issue when logging non-ASCII parameters supplied to CuraEngine + * [#1891](https://github.com/foosel/OctoPrint/issues/1891) - Fixed error when handling unicode passwords. + * [#1893](https://github.com/foosel/OctoPrint/issues/1893) - CuraEngine plugin: Fixed handling of multiple consecutive uploads of slicing profiles (see also [#1894](https://github.com/foosel/OctoPrint/issues/1894)) + * [#1897](https://github.com/foosel/OctoPrint/issues/1897) - Removed possibility to concurrently try to perform multiple tests of the configured snapshot URL. + * [#1906](https://github.com/foosel/OctoPrint/issues/1906) - Fixed interpretation of `G92` in GCODE analysis. + * [#1907](https://github.com/foosel/OctoPrint/issues/1907) - Don't send temperature commands with tool parameter when a shared nozzle is defined. + * [#1917](https://github.com/foosel/OctoPrint/issues/1917) (regression) - Fix job data resetting on print job completion. + * [#1918](https://github.com/foosel/OctoPrint/issues/1918) (regression) - Fix "save as default" checkbox not being disabled when other controls are disabled. + * [#1919](https://github.com/foosel/OctoPrint/issues/1919) (regression) - Fix call to no longer existing function in Plugin Manager UI. + * [#1934](https://github.com/foosel/OctoPrint/issues/1934) (regression) - Fix consecutive timed timelapse captures without configured post roll. + * Fixed API key QR Code being shown (for "n/a" value) when no API key was set. + * Fixed timelapse configuration API not returning 400 status code on some bad parameters. + * Fixed a typo (see also [#1826](https://github.com/foosel/OctoPrint/pull/1826)). + * Fixed `filter` and `force` parameters on `/api/files/`. + * Fixed message catchall `*` not working in the socket client library. + * Fixed analysis backlog calculation for sub folders. + * Fixed `PrinterInterface.is_ready` to behave as documented. + * Use black listing instead of white listing again to detect if the `daemon` sub command is supported or not. Should resolve issues users of FreeBSD and the like where having with `octoprint daemon`. + * Use `pip` instead of `python setup.py develop` in `octoprint dev plugin:install` command to avoid issues on Windows. + * Docs: Fixed a wrong command in the plugin tutorial (see also [#1860](https://github.com/foosel/OctoPrint/pull/1860)). + +### More information + +- [Commits](https://github.com/foosel/OctoPrint/compare/1.3.2...1.3.3) +- Release Candidates: + - [1.3.3rc1](https://github.com/foosel/OctoPrint/releases/tag/1.3.3rc1) + - [1.3.3rc2](https://github.com/foosel/OctoPrint/releases/tag/1.3.3rc2) + - [1.3.3rc3](https://github.com/foosel/OctoPrint/releases/tag/1.3.3rc3) + ## 1.3.2 (2017-03-16) ### Note for plugin authors diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1f80202215..b8ba81c03f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -142,6 +142,12 @@ only provided here as some additional information for you), **even if only addin reproduce your issue in order to fix it and that is not possible if they don't know what you did to get it to happen in the first place. + Ideally provide exact steps to follow in order to reproduce your problem: + + 1. ... + 2. ... + 3. ... + If you encountered a problem with specific files of any sorts, make sure to also include a link to a file with which to reproduce the problem.] @@ -149,10 +155,20 @@ only provided here as some additional information for you), **even if only addin #### What happened instead? + #### Did the same happen when running OctoPrint in safe mode? + + [Try to reproduce your problem in safe mode. You can find information + on how to enable safe mode in the Contribution Guidelines.] + #### Branch & Commit or Version of OctoPrint [Can be found in the lower left corner of the web interface. ALWAYS INCLUDE.] + #### Operating System running OctoPrint + + [OctoPi, Linux, Windows, MacOS, something else? With version please, + OctoPi's version can be found in /etc/octopi_version] + #### Printer model & used firmware incl. version [If applicable, always include if unsure.] @@ -163,7 +179,8 @@ only provided here as some additional information for you), **even if only addin #### Link to octoprint.log - [On gist.github.com or pastebin.com. ALWAYS INCLUDE and never truncate.] + [On gist.github.com or pastebin.com. ALWAYS INCLUDE and never truncate. + The Contribution Guidelines tell you where to find that.] #### Link to contents of terminal tab or serial.log @@ -178,16 +195,19 @@ only provided here as some additional information for you), **even if only addin #### Link to contents of Javascript console in the browser [On gist.github.com or pastebin.com or alternatively a screenshot. If applicable - - always include if unsure or reporting UI issues.] + always include if unsure or reporting UI issues. + + The Contribution Guidelines tell you where to find that.] - #### Screenshot(s) showing the problem: + #### Screenshot(s)/video(s) showing the problem: [If applicable. Always include if unsure or reporting UI issues.] I have read the FAQ. -Copy-paste this template **completely**. Do not skip any lines or the bot -*will* complain! +Copy-paste this template **completely** (or use the version that gets pre-filled +into the "new issue" form). Do not skip any lines or the bot *will* complain! Provide +all requested information or your ticket will be closed. ### Where can I find which version and branch I'm on? @@ -345,10 +365,16 @@ There are three main branches in OctoPrint: the `maintenance` branch and are now being pushed on the "Maintenance" pre release channel for further testing. Version number follows the scheme `..rc` (e.g. `1.2.9rc1`). + * `staging/maintenance`: Any preparation for potential follow-up RCs takes place here. + Version number follows the scheme `..rc.dev` (e.g. + `1.2.9rc1.dev3`) for a current Maintenance RC of `..rc`. * `rc/devel`: This branch is reserved for future releases that have graduated from the `devel` branch and are now being pushed on the "Devel" pre release channel for further testing. Version number follows the scheme `..0rc` (e.g. `1.3.0rc1`) for a current stable OctoPrint version of `..`. + * `staging/devel`: Any preparation for potential follow-up Devel RCs takes place + here. Version number follows the scheme `..0rc.dev` (e.g. + `1.3.0rc1.dev12`) for a current Devel RC of `..0rc`. Additionally, from time to time you might see other branches pop up in the repository. Those usually have one of the following prefixes: @@ -360,7 +386,7 @@ Those usually have one of the following prefixes: * `dev/...` or `feature/...`: New functionality under development that is to be merged into the `devel` branch. -There is also the `gh-pages` branch, which holds OctoPrint's web page, and a couple of +There is also the `gh-pages` branch, which holds OctoPrint's web page, and a few older development branches that are slowly being migrated or deleted. ## How OctoPrint is versioned @@ -405,6 +431,7 @@ the local version identifier to allow for an exact determination of the active c * 2017-03-09: Allow PRs against `maintenance` branch for bugs in stable. * 2017-03-10: Reproduce bugs in safe mode to make sure they are really caused by OctoPrint itself and not a misbehaving plugin. + * 2017-03-27: Added safe mode section to ticket template. ## Footnotes * [1] - If you are wondering why, the problem is that anything that you add diff --git a/README.md b/README.md index dfe174be24..817884a57d 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ and at its settings. ## Dependencies -OctoPrint depends on a couple of python modules to do its job. Those are automatically installed when installing +OctoPrint depends on a few python modules to do its job. Those are automatically installed when installing OctoPrint via `setup.py`: python setup.py install diff --git a/SUPPORTERS.md b/SUPPORTERS.md index 8fd0854169..2909b81013 100644 --- a/SUPPORTERS.md +++ b/SUPPORTERS.md @@ -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/) - @@ -10,11 +10,11 @@ thanks to everyone who contributed! * Aleph Objects, Inc. * Andrew Moorby * Arnljot Arntsen + * BEEVERYCREATIVE * Boris Hussein * Brad Jackson - * Brent Fiegle * Brian E. Tyler - * Christopher Day + * Chris Day * Christian Petropolis * CreativeTools * D Brian Kimmel @@ -23,13 +23,15 @@ thanks to everyone who contributed! * E3D BigBox * Ernesto Martinez * Exovite + * F. Kunsmann * Frank Sander * Gary Deen * Gary N McKinney * George Robles + * günter weber + * Ivan Krasin * James Seigel * Jamie R McGuigan - * Jamie van Dyke * Jeff Moe * Josh Daniels * Kaile Riser @@ -59,9 +61,8 @@ thanks to everyone who contributed! * Stefan Krister * Stephane Schittly * Sven Mueller - * Terrance Shaw * Thomas Hatley * Timeshell.ca * Trent Shumay -and 1041 more wonderful people pledging on the [Patreon campaign](https://patreon.com/foosel)! +and 1098 more wonderful people pledging on the [Patreon campaign](https://patreon.com/foosel)! \ No newline at end of file diff --git a/docs/api/datamodel.rst b/docs/api/datamodel.rst index 076e7dbe4e..768e420b03 100644 --- a/docs/api/datamodel.rst +++ b/docs/api/datamodel.rst @@ -31,7 +31,7 @@ Printer State * - ``flags`` - 1 - Printer state flags - - A couple of boolean printer state flags + - A few boolean printer state flags * - ``flags.operational`` - 1 - Boolean diff --git a/docs/api/settings.rst b/docs/api/settings.rst index 345a44135e..acc797220e 100644 --- a/docs/api/settings.rst +++ b/docs/api/settings.rst @@ -68,6 +68,23 @@ Save settings // ... } +.. _sec-api-settings-generateapikey: + +Regenerate the system wide API key +================================== + +.. http:post:: /api/settings/apikey + + Generates a new system wide API key. + + Does not expect a body. Will return the generated API key as ``apikey`` + property in the JSON object contained in the response body. + + Requires admin rights. + + :status 200: No error + :status 403: No admin rights + .. _sec-api-settings-datamodel: Data model @@ -87,6 +104,9 @@ mapped from the same fields in ``config.yaml`` unless otherwise noted: - * - ``api.key`` - Only maps to ``api.key`` in ``config.yaml`` if request is sent with admin rights, set to ``n/a`` otherwise. + Starting with OctoPrint 1.3.3 setting this field via :ref:`the API ` is not possible, + only :ref:`regenerting it ` is supported. Setting a custom value is only + possible through `config.yaml`. * - ``api.allowCrossOrigin`` - * - ``appearance.name`` diff --git a/docs/bundledplugins/softwareupdate.rst b/docs/bundledplugins/softwareupdate.rst index a4e1213eee..17efc535c3 100644 --- a/docs/bundledplugins/softwareupdate.rst +++ b/docs/bundledplugins/softwareupdate.rst @@ -188,6 +188,16 @@ Version checks * ``repo``: (mandatory) Github repository to check * ``branch``: Branch of the Github repository to check, defaults to ``master`` if not set. + * ``current``: Current commit hash. Will be updated automatically. + + * ``bitbucket_commit``: Checks against commits pushed to Bitbucket. Additional + config parameters: + + * ``user``: (mandatory) Bitbucket user the repository to check belongs to + * ``repo``: (mandatory) Bitbucket repository to check + * ``branch``: Branch of the Bitbucket repository to check, defaults to + ``master`` if not set. + * ``current``: Current commit hash. Will be updated automatically. * ``git_commit``: Checks a local git repository for new commits on its configured remote. Additional config parameters: diff --git a/docs/configuration/config_yaml.rst b/docs/configuration/config_yaml.rst index c48389ae82..debb35d166 100644 --- a/docs/configuration/config_yaml.rst +++ b/docs/configuration/config_yaml.rst @@ -943,8 +943,8 @@ Use the following settings to configure the server: * ``X-Scheme``: should contain your custom URL scheme to use (if different from ``http``), e.g. ``https`` If you use these headers OctoPrint will work both via the reverse proxy as well as when called directly. Take a look - `into OctoPrint's wiki `_ for a couple - of examples on how to configure this. + `into OctoPrint's wiki `_ for some + examples on how to configure this. .. _sec-configuration-config_yaml-slicing: diff --git a/docs/events/index.rst b/docs/events/index.rst index 802ed3856a..b471193f5a 100644 --- a/docs/events/index.rst +++ b/docs/events/index.rst @@ -152,7 +152,7 @@ File handling ------------- Upload - A file has been uploaded. + A file has been uploaded through the web interface. Payload: * ``name``: the file's name @@ -165,19 +165,74 @@ Upload Still available for reasons of backwards compatibility. Will be removed with 1.4.0. +FileAdded + A file has been added to a storage. + + Payload: + * ``storage``: the storage's identifier + * ``path``: the file's path within its storage location + * ``name``: the file's name + * ``type``: the file's type, a list of the path within the type hierarchy, e.g. ``["machinecode", "gcode"]`` or + ``["model", "stl"]`` + + .. note:: + + A copied file triggers this for its new path. A moved file first triggers ``FileRemoved`` for its original + path and then ``FileAdded`` for the new one. + +FileRemoved + A file has been removed from a storage. + + Payload: + * ``storage``: the storage's identifier + * ``path``: the file's path within its storage location + * ``name``: the file's name + * ``type``: the file's type, a list of the path within the type hierarchy, e.g. ``["machinecode", "gcode"]`` or + ``["model", "stl"]`` + + .. note:: + + A moved file first triggers ``FileRemoved`` for its original path and then ``FileAdded`` for the new one. + +FolderAdded + A folder has been added to a storage. + + Payload: + * ``storage``: the storage's identifier + * ``path``: the folders's path within its storage location + * ``name``: the folders's name + + .. note:: + + A copied folder triggers this for its new path. A moved folder first triggers ``FolderRemoved`` for its original + path and then ``FolderAdded`` for the new one. + +FolderRemoved + A folder has been removed from a storage. + + Payload: + * ``storage``: the storage's identifier + * ``path``: the folders's path within its storage location + * ``name``: the folders's name + + .. note:: + + A moved folder first triggers ``FolderRemoved`` for its original path and then ``FolderAdded`` for the new one. + UpdatedFiles A file list was modified. Payload: - * ``type``: the type of file list that was modified. Currently only ``printables`` and ``gcode`` (DEPRECATED) are supported here. + * ``type``: the type of file list that was modified. Only ``printables`` is supported here. See the deprecation + note below. + + .. deprecated:: 1.2.0 - .. note:: + The ``gcode`` modification type has been superceeded by ``printables``. It is currently still available for + reasons of backwards compatibility and will also be sent on modification of ``printables``. It will however + be removed with 1.4.0. - The type ``gcode`` has been renamed to ``printables`` with the introduction of a new file management layer that - supports STL files as first class citizens as well. For reasons of backwards compatibility the ``UpdatedFiles`` - event for printable files will be fired twice, once with ``type`` set to ``gcode``, once set to ``printables``. - Support for the ``gcode`` type will be removed in the next release after version 1.2.0. MetadataAnalysisStarted The metadata analysis of a file has started. diff --git a/docs/features/gcode_scripts.rst b/docs/features/gcode_scripts.rst index e58cfdbc5b..ee3136da85 100644 --- a/docs/features/gcode_scripts.rst +++ b/docs/features/gcode_scripts.rst @@ -13,11 +13,19 @@ Unless :ref:`configured otherwise `, OctoP the ``scripts/gcode`` folder in OctoPrint configuration directory (per default ``~/.octoprint`` on Linux, ``%APPDATA%\OctoPrint`` on Windows and ``~/Library/Application Support/OctoPrint`` on MacOS). -These GCODE scripts are backed by the templating engine `Jinja2 `_, allowing more than just +These GCODE scripts are backed by the templating engine Jinja2, allowing more than just simple "send-as-is" scripts but making use of a full blown templating language in order to create your scripts. To -this end, OctoPrint injects a couple of variables into the :ref:`template rendering context ` +this end, OctoPrint injects some variables into the :ref:`template rendering context ` as described below. +You can find the docs on the Jinja templating engine as used in OctoPrint at `jinja.octoprint.org `_. + +.. note:: + + Due to backwards compatibility issues with Jinja versions 2.9+, OctoPrint currently only supports Jinja 2.8. For this + reason use the template documentation at `jinja.octoprint.org `_ instead of the + documentation of current stable Jinja versions. + .. _sec-features-gcode_scripts-predefined: Predefined Scripts @@ -64,19 +72,21 @@ All GCODE scripts have access to the following template variables through the te * ``script``: An object wrapping the script's type (``gcode``) and name (e.g. ``afterPrintCancelled``) as ``script.type`` and ``script.name`` respectively. -There are a couple of additional template variables available for the following specific scripts: +There are a few additional template variables available for the following specific scripts: * ``afterPrintPaused`` and ``beforePrintResumed`` * ``pause_position``: Position reported by the printer via ``M114`` immediately before the print was paused. Consists of ``x``, ``y``, ``z`` and ``e`` coordinates as received by the printer and tracked values for ``f`` and current tool - ``t`` taken from commands sent through OctoPrint. + ``t`` taken from commands sent through OctoPrint. All of these coordinates might be ``None`` if no position could be + retrieved from the printer or the values could not be tracked (in case of ``f`` and ``t``)! * ``afterPrintCancelled`` * ``cancel_position``: Position reported by the printer via ``M114`` immediately before the print was cancelled. Consists of ``x``, ``y``, ``z`` and ``e`` coordinates as received by the printer and tracked values for ``f`` and - current tool ``t`` taken from commands sent through OctoPrint. + current tool ``t`` taken from commands sent through OctoPrint. All of these coordinates might be ``None`` if no + position could be retrieved from the printer or the values could not be tracked (in case of ``f`` and ``t``)! .. warning:: @@ -150,6 +160,7 @@ to 0 if a heated bed is configured. .. seealso:: - `Jinja Template Designer Documentation `_ + `Jinja Template Designer Documentation `_ Jinja's Template Designer Documentation describes the syntax and semantics of the template language used - also by OctoPrint's GCODE scripts. + also by OctoPrint's GCODE scripts. Linked here are the docs for Jinja 2.8.1, which OctoPrint still + relies on for backwards compatibility reasons. diff --git a/docs/jsclientlib/settings.rst b/docs/jsclientlib/settings.rst index ad804b8b2f..a889471863 100644 --- a/docs/jsclientlib/settings.rst +++ b/docs/jsclientlib/settings.rst @@ -35,6 +35,13 @@ :param object opts: Additional options for the request :returns Promise: A `jQuery Promise `_ for the request's response +.. js:function:: OctoPrintClient.settings.generateApiKey(opts) + + Generate a new system wide API key. + + :param object opts: Additional options for the request + :returns Promise: A `jQuery Promise `_ for the request's response + .. seealso:: :ref:`Settings API ` diff --git a/docs/plugins/gettingstarted.rst b/docs/plugins/gettingstarted.rst index 6bb5549215..ca4f3935d1 100644 --- a/docs/plugins/gettingstarted.rst +++ b/docs/plugins/gettingstarted.rst @@ -20,7 +20,7 @@ development environment:: $ virtualenv venv [...] $ source venv/bin/activate - (venv) $ pip install -e[develop] + (venv) $ pip install -e .[develop] [...] (venv) $ octoprint --help Usage: octoprint [OPTIONS] COMMAND [ARGS]... @@ -41,7 +41,15 @@ development environment:: [...] -We'll start at the most basic form a plugin can take - just a couple of simple lines of Python code: +.. important:: + + This tutorial assumes you are running OctoPrint 1.3.0 and up. Please make sure your version of + OctoPrint is up to date before proceeding. If you did a fresh checkout, that should already + be the case but if not you might have to update first. You can check your version of OctoPrint + by running ``octoprint --version`` or by taking a look into the lower left corner in OctoPrint's + web interface. + +We'll start at the most basic form a plugin can take - just a few simple lines of Python code: .. code-block:: python :linenos: @@ -108,7 +116,7 @@ used :func:`~octoprint.plugin.StartupPlugin.on_startup` instead, in which case o up and ready to serve requests. You'll also note that we are using ``self._logger`` for logging. Where did that one come from? OctoPrint's plugin system -injects :ref:`a couple of useful objects ` into our plugin implementation classes, +injects :ref:`a some useful objects ` into our plugin implementation classes, one of those being a fully instantiated `python logger `_ ready to be used by your plugin. As you can see in the log output above, that logger uses the namespace ``octoprint.plugins.helloworld`` for our little plugin here, or more generally ``octoprint.plugins.``. @@ -482,7 +490,7 @@ Also adjust your plugin's ``templates/helloworld_navbar.jinja2`` like this: OctoPrint injects the template variables that your plugin defines prefixed with ``plugin__`` into the template renderer, so your ``url`` got turned into ``plugin_helloworld_url`` which you can now use as a simple -`Jinja2 Variable `_ in your plugin's template. +`Jinja2 Variable `_ in your plugin's template. Restart OctoPrint and shift-reload the page in your browser (to make sure you really get a fresh copy). The link should still work and point to the URL we defined as default. @@ -1070,8 +1078,14 @@ For some insight on how to create plugins that react to various events within Oc add support for a slicer, OctoPrint's own bundled `CuraEngine plugin `_ might give some hints. For extending OctoPrint's interface, the `NavbarTemp plugin `_ might show what's possible with a few lines of code already. Finally, just take a look at the -`list of available plugins `_ on the OctoPrint wiki if you are -looking for examples. +`official Plugin Repository `_ if you are looking for examples. + +.. seealso:: + + `Jinja Template Designer Documentation `_ + Jinja's Template Designer Documentation describes the syntax and semantics of the template language used + by OctoPrint's frontend. Linked here are the docs for Jinja 2.8.1, which OctoPrint still + relies on for backwards compatibility reasons [#f3]_. .. rubric:: Footnotes @@ -1084,3 +1098,7 @@ looking for examples. .. [#f2] Refer to the `LESS documentation `_ on how to do that. If you are developing your plugin under Windows you might also want to give `WinLESS `_ a look which will run in the background and keep your CSS files up to date with your various project's LESS files automatically. +.. [#f3] Please always consult the Jinja documentation at `jinja.octoprint.org `_ instead of + the current stable documentation available at Jinja's project page. The reason for that is that for backwards + compatibility reasons OctoPrint currently sadly has to rely on an older version of Jinja. The documentation + available at `jinja.octoprint.org `_ matches that older version. diff --git a/docs/plugins/helpers.rst b/docs/plugins/helpers.rst index 047811d8be..17cd182897 100644 --- a/docs/plugins/helpers.rst +++ b/docs/plugins/helpers.rst @@ -6,7 +6,7 @@ Helpers Helpers are methods that plugin can exposed to other plugins in order to make common functionality available on the system. They are registered with the OctoPrint plugin system through the use of the control property ``__plugin_helpers__``. -An example for providing a couple of helper functions to the system can be found in the +An example for providing some helper functions to the system can be found in the `Discovery Plugin `_, which provides it's SSDP browsing and Zeroconf browsing and publishing functions as helper methods. diff --git a/setup.cfg b/setup.cfg index cbda2efbf4..9be46c443c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,7 +3,7 @@ description-file = README.md [versioneer] VCS = git -style = pep440-post +style = pep440-tag versionfile_source = src/octoprint/_version.py versionfile_build = octoprint/_version.py tag_prefix = diff --git a/setup.py b/setup.py index 71b81f0ddd..c6eabbd09d 100644 --- a/setup.py +++ b/setup.py @@ -47,7 +47,8 @@ "chainmap>=1.0.2,<1.1", "future>=0.15,<0.16", "scandir>=1.3,<1.4", - "websocket-client>=0.40,<0.41" + "websocket-client>=0.40,<0.41", + "python-dateutil>=2.6,<2.7" ] if sys.platform == "darwin": diff --git a/src/octoprint/cli/__init__.py b/src/octoprint/cli/__init__.py index 6d17f4a424..7a4133ce9f 100644 --- a/src/octoprint/cli/__init__.py +++ b/src/octoprint/cli/__init__.py @@ -160,11 +160,11 @@ def get_value(key): "start|stop|restart\" is deprecated, please use " "\"octoprint daemon start|stop|restart\" from now on") - if sys.platform == "linux2": + if sys.platform == "win32" or sys.platform == "darwin": + click.echo("Sorry, daemon mode is not supported under your operating system right now") + else: from octoprint.cli.server import daemon_command ctx.invoke(daemon_command, command=daemon, **kwargs) - else: - click.echo("Sorry, daemon mode is only supported under Linux right now") else: click.echo("Starting the server via \"octoprint\" is deprecated, " "please use \"octoprint serve\" from now on.") diff --git a/src/octoprint/cli/client.py b/src/octoprint/cli/client.py index cafcd1817b..97fcce652a 100644 --- a/src/octoprint/cli/client.py +++ b/src/octoprint/cli/client.py @@ -29,6 +29,7 @@ def client_commands(): @client_commands.group("client", context_settings=dict(ignore_unknown_options=True)) +@click.option("--apikey", "-a", type=click.STRING) @click.option("--host", "-h", type=click.STRING) @click.option("--port", "-p", type=click.INT) @click.option("--httpuser", type=click.STRING) @@ -36,11 +37,29 @@ def client_commands(): @click.option("--https", is_flag=True) @click.option("--prefix", type=click.STRING) @click.pass_context -def client(ctx, host, port, httpuser, httppass, https, prefix): +def client(ctx, apikey, host, port, httpuser, httppass, https, prefix): """Basic API client.""" try: - ctx.obj.settings = init_settings(get_ctx_obj_option(ctx, "basedir", None), get_ctx_obj_option(ctx, "configfile", None)) - octoprint_client.init_client(ctx.obj.settings, https=https, httpuser=httpuser, httppass=httppass, host=host, port=port, prefix=prefix) + if not host or not port or not apikey: + settings = init_settings(get_ctx_obj_option(ctx, "basedir", None), get_ctx_obj_option(ctx, "configfile", None)) + + if not host: + host = settings.get(["server", "host"]) + host = host if host != "0.0.0.0" else "127.0.0.1" + if not port: + port = settings.getInt(["server", "port"]) + + if not apikey: + apikey = settings.get(["api", "key"]) + + baseurl = octoprint_client.build_base_url(https=https, + httpuser=httpuser, + httppass=httppass, + host=host, + port=port, + prefix=prefix) + + ctx.obj.client = octoprint_client.Client(baseurl, apikey) except FatalStartupError as e: click.echo(e.message, err=True) click.echo("There was a fatal error initializing the client.", err=True) @@ -60,27 +79,33 @@ def log_response(response, status_code=True, body=True, headers=False): @client.command("get") @click.argument("path") -def get(path): +@click.option("--timeout", type=float, default=None) +@click.pass_context +def get(ctx, path, timeout): """Performs a GET request against the specified server path.""" - r = octoprint_client.get(path) + r = ctx.obj.client.get(path, timeout=timeout) log_response(r) @client.command("post_json") @click.argument("path") @click.argument("data", type=JsonStringParamType()) -def post_json(path, data): +@click.option("--timeout", type=float, default=None) +@click.pass_context +def post_json(ctx, path, data, timeout): """POSTs JSON data to the specified server path.""" - r = octoprint_client.post_json(path, data) + r = ctx.obj.client.post_json(path, data, timeout=timeout) log_response(r) @client.command("patch_json") @click.argument("path") @click.argument("data", type=JsonStringParamType()) -def patch_json(path, data): +@click.option("--timeout", type=float, default=None, help="Request timeout in seconds") +@click.pass_context +def patch_json(ctx, path, data, timeout): """PATCHes JSON data to the specified server path.""" - r = octoprint_client.patch(path, data, encoding="json") + r = ctx.obj.client.patch(path, data, encoding="json", timeout=timeout) log_response(r) @@ -89,8 +114,10 @@ def patch_json(path, data): @click.argument("file_path", type=click.Path(exists=True, dir_okay=False, resolve_path=True)) @click.option("--json", is_flag=True) @click.option("--yaml", is_flag=True) -def post_from_file(path, file_path, json_flag, yaml_flag): - """POSTs JSON data to the specified server path.""" +@click.option("--timeout", type=float, default=None, help="Request timeout in seconds") +@click.pass_context +def post_from_file(ctx, path, file_path, json_flag, yaml_flag, timeout): + """POSTs JSON data to the specified server path, taking the data from the specified file.""" if json_flag or yaml_flag: if json_flag: with open(file_path, "rb") as fp: @@ -100,12 +127,12 @@ def post_from_file(path, file_path, json_flag, yaml_flag): with open(file_path, "rb") as fp: data = yaml.safe_load(fp) - r = octoprint_client.post_json(path, data) + r = ctx.obj.client.post_json(path, data, timeout=timeout) else: with open(file_path, "rb") as fp: data = fp.read() - r = octoprint_client.post(path, data) + r = ctx.obj.client.post(path, data, timeout=timeout) log_response(r) @@ -117,13 +144,15 @@ def post_from_file(path, file_path, json_flag, yaml_flag): @click.option("--int", "-i", "int_params", multiple=True, nargs=2, type=click.Tuple([unicode, int])) @click.option("--float", "-f", "float_params", multiple=True, nargs=2, type=click.Tuple([unicode, float])) @click.option("--bool", "-b", "bool_params", multiple=True, nargs=2, type=click.Tuple([unicode, bool])) -def command(path, command, str_params, int_params, float_params, bool_params): +@click.option("--timeout", type=float, default=None, help="Request timeout in seconds") +@click.pass_context +def command(ctx, path, command, str_params, int_params, float_params, bool_params, timeout): """Sends a JSON command to the specified server path.""" data = dict() params = str_params + int_params + float_params + bool_params for param in params: data[param[0]] = param[1] - r = octoprint_client.post_command(path, command, additional=data) + r = ctx.obj.client.post_command(path, command, additional=data, timeout=timeout) log_response(r, body=False) @@ -133,26 +162,32 @@ def command(path, command, str_params, int_params, float_params, bool_params): @click.option("--parameter", "-P", "params", multiple=True, nargs=2, type=click.Tuple([unicode, unicode])) @click.option("--file-name", type=click.STRING) @click.option("--content-type", type=click.STRING) -def upload(path, file_path, params, file_name, content_type): +@click.option("--timeout", type=float, default=None, help="Request timeout in seconds") +@click.pass_context +def upload(ctx, path, file_path, params, file_name, content_type, timeout): """Uploads the specified file to the specified server path.""" data = dict() for param in params: data[param[0]] = param[1] - r = octoprint_client.upload(path, file_path, additional=data, file_name=file_name, content_type=content_type) + r = ctx.obj.client.upload(path, file_path, + additional=data, file_name=file_name, content_type=content_type, timeout=timeout) log_response(r) @client.command("delete") @click.argument("path") -def delete(path): +@click.option("--timeout", type=float, default=None, help="Request timeout in seconds") +@click.pass_context +def delete(ctx, path, timeout): """Sends a DELETE request to the specified server path.""" - r = octoprint_client.delete(path) + r = ctx.obj.client.delete(path, timeout=timeout) log_response(r) @client.command("listen") -def listen(): +@click.pass_context +def listen(ctx): def on_connect(ws): click.echo(">>> Connected!") @@ -168,11 +203,11 @@ def on_heartbeat(ws): def on_message(ws, message_type, message_payload): click.echo("Message: {}, Payload: {}".format(message_type, json.dumps(message_payload))) - socket = octoprint_client.connect_socket(on_connect=on_connect, - on_close=on_close, - on_error=on_error, - on_heartbeat=on_heartbeat, - on_message=on_message) + socket = ctx.obj.client.create_socket(on_connect=on_connect, + on_close=on_close, + on_error=on_error, + on_heartbeat=on_heartbeat, + on_message=on_message) click.echo(">>> Waiting for client to exit") try: diff --git a/src/octoprint/cli/dev.py b/src/octoprint/cli/dev.py index baf678bcc3..7273b8ba00 100644 --- a/src/octoprint/cli/dev.py +++ b/src/octoprint/cli/dev.py @@ -204,7 +204,7 @@ def command(path): click.echo("This doesn't look like an OctoPrint plugin folder") sys.exit(1) - self.command_caller.call([sys.executable, "setup.py", "develop"], cwd=path) + self.command_caller.call([sys.executable, "-m", "pip", "install", "-e", "."], cwd=path) return command diff --git a/src/octoprint/cli/server.py b/src/octoprint/cli/server.py index ca2d93ef68..cfadc559b9 100644 --- a/src/octoprint/cli/server.py +++ b/src/octoprint/cli/server.py @@ -150,8 +150,8 @@ def get_value(key): allow_root, logging, verbosity, safe_mode) -if sys.platform == "linux2": - # we only support daemon mode under Linux +if sys.platform != "win32" and sys.platform != "darwin": + # we do not support daemon mode under windows or macosx @server_commands.command(name="daemon") @server_options diff --git a/src/octoprint/events.py b/src/octoprint/events.py index eb500a0149..c8630e59af 100644 --- a/src/octoprint/events.py +++ b/src/octoprint/events.py @@ -53,6 +53,11 @@ class Events(object): METADATA_ANALYSIS_FINISHED = "MetadataAnalysisFinished" METADATA_STATISTICS_UPDATED = "MetadataStatisticsUpdated" + FILE_ADDED = "FileAdded", + FILE_REMOVED = "FileRemoved", + FOLDER_ADDED = "FolderAdded", + FOLDER_REMOVED = "FolderRemoved", + # SD Upload TRANSFER_STARTED = "TransferStarted" TRANSFER_DONE = "TransferDone" @@ -350,7 +355,10 @@ def process(): else: commandExecutioner(command) except subprocess.CalledProcessError as e: - self._logger.warn("Command failed with return code %i: %s" % (e.returncode, str(e))) + if debug: + self._logger.warn("Command failed with return code {}: {}".format(e.returncode, str(e))) + else: + self._logger.warn("Command failed with return code {}, enable debug logging on target 'octoprint.events' for details".format(e.returncode)) except: self._logger.exception("Command failed") @@ -368,7 +376,7 @@ def _executeGcodeCommand(self, command, debug=False): def _processCommand(self, command, payload): """ - Performs string substitutions in the command string based on a couple of current parameters. + Performs string substitutions in the command string based on a few current parameters. The following substitutions are currently supported: diff --git a/src/octoprint/filemanager/__init__.py b/src/octoprint/filemanager/__init__.py index de06893b72..35c9075e46 100644 --- a/src/octoprint/filemanager/__init__.py +++ b/src/octoprint/filemanager/__init__.py @@ -409,30 +409,47 @@ def add_file(self, destination, path, file_object, links=None, allow_overwrite=F queue_entry = self._analysis_queue_entry(destination, path) self._analysis_queue.dequeue(queue_entry) - file_path = self._storage(destination).add_file(path, file_object, links=links, printer_profile=printer_profile, allow_overwrite=allow_overwrite) + path_in_storage = self._storage(destination).add_file(path, file_object, links=links, printer_profile=printer_profile, allow_overwrite=allow_overwrite) if analysis is None: - queue_entry = self._analysis_queue_entry(destination, file_path, printer_profile=printer_profile) + queue_entry = self._analysis_queue_entry(destination, path_in_storage, printer_profile=printer_profile) if queue_entry: self._analysis_queue.enqueue(queue_entry, high_priority=True) else: self._add_analysis_result(destination, path, analysis) + _, name = self._storage(destination).split_path(path_in_storage) + eventManager().fire(Events.FILE_ADDED, dict(storage=destination, + path=path_in_storage, + name=name, + type=get_file_type(name))) eventManager().fire(Events.UPDATED_FILES, dict(type="printables")) - return file_path + return path_in_storage def remove_file(self, destination, path): queue_entry = self._analysis_queue_entry(destination, path) self._analysis_queue.dequeue(queue_entry) self._storage(destination).remove_file(path) + + _, name = self._storage(destination).split_path(path) + eventManager().fire(Events.FILE_REMOVED, dict(storage=destination, + path=path, + name=name, + type=get_file_type(name))) eventManager().fire(Events.UPDATED_FILES, dict(type="printables")) def copy_file(self, destination, source, dst): - path = self._storage(destination).copy_file(source, dst) - if not self.has_analysis(destination, path): - queue_entry = self._analysis_queue_entry(destination, path) + path_in_storage = self._storage(destination).copy_file(source, dst) + if not self.has_analysis(destination, path_in_storage): + queue_entry = self._analysis_queue_entry(destination, path_in_storage) if queue_entry: self._analysis_queue.enqueue(queue_entry) + + _, name = self._storage(destination).split_path(path_in_storage) + eventManager().fire(Events.FILE_ADDED, dict(storage=destination, + path=path_in_storage, + name=name, + type=get_file_type(name))) eventManager().fire(Events.UPDATED_FILES, dict(type="printables")) def move_file(self, destination, source, dst): @@ -443,23 +460,52 @@ def move_file(self, destination, source, dst): queue_entry = self._analysis_queue_entry(destination, path) if queue_entry: self._analysis_queue.enqueue(queue_entry) + + source_path_in_storage = self._storage(destination).path_in_storage(source) + _, source_name = self._storage(destination).split_path(source_path_in_storage) + dst_path_in_storage = self._storage(destination).path_in_storage(dst) + _, dst_name = self._storage(destination).split_path(dst_path_in_storage) + + eventManager().fire(Events.FILE_REMOVED, dict(storage=destination, + path=source_path_in_storage, + name=source_name, + type=get_file_type(source_name))) + eventManager().fire(Events.FILE_ADDED, dict(storage=destination, + path=dst_path_in_storage, + name=dst_name, + type=get_file_type(dst_name))) eventManager().fire(Events.UPDATED_FILES, dict(type="printables")) def add_folder(self, destination, path, ignore_existing=True): - folder_path = self._storage(destination).add_folder(path, ignore_existing=ignore_existing) + path_in_storage = self._storage(destination).add_folder(path, ignore_existing=ignore_existing) + + _, name = self._storage(destination).split_path(path_in_storage) + eventManager().fire(Events.FOLDER_ADDED, dict(storage=destination, + path=path_in_storage, + name=name)) eventManager().fire(Events.UPDATED_FILES, dict(type="printables")) - return folder_path + return path_in_storage def remove_folder(self, destination, path, recursive=True): self._analysis_queue.dequeue_folder(destination, path) self._analysis_queue.pause() self._storage(destination).remove_folder(path, recursive=recursive) self._analysis_queue.resume() + + _, name = self._storage(destination).split_path(path) + eventManager().fire(Events.FOLDER_REMOVED, dict(storage=destination, + path=path, + name=name)) eventManager().fire(Events.UPDATED_FILES, dict(type="printables")) def copy_folder(self, destination, source, dst): - self._storage(destination).copy_folder(source, dst) + path_in_storage = self._storage(destination).copy_folder(source, dst) self._determine_analysis_backlog(destination, self._storage(destination), root=dst) + + _, name = self._storage(destination).split_path(path_in_storage) + eventManager().fire(Events.FOLDER_ADDED, dict(storage=destination, + path=path_in_storage, + name=name)) eventManager().fire(Events.UPDATED_FILES, dict(type="printables")) def move_folder(self, destination, source, dst): @@ -468,6 +514,18 @@ def move_folder(self, destination, source, dst): self._storage(destination).move_folder(source, dst) self._determine_analysis_backlog(destination, self._storage(destination), root=dst) self._analysis_queue.resume() + + source_path_in_storage = self._storage(destination).path_in_storage(source) + _, source_name = self._storage(destination).split_path(source_path_in_storage) + dst_path_in_storage = self._storage(destination).path_in_storage(destination) + _, dst_name = self._storage(destination).split_path(dst_path_in_storage) + + eventManager().fire(Events.FOLDER_REMOVED, dict(storage=destination, + path=source_path_in_storage, + name=source_name)) + eventManager().fire(Events.FOLDER_ADDED, dict(storage=destination, + path=dst_path_in_storage, + name=dst_name)) eventManager().fire(Events.UPDATED_FILES, dict(type="printables")) def has_analysis(self, destination, path): diff --git a/src/octoprint/filemanager/storage.py b/src/octoprint/filemanager/storage.py index 9a74e3dc3e..c69b3e14c3 100644 --- a/src/octoprint/filemanager/storage.py +++ b/src/octoprint/filemanager/storage.py @@ -29,7 +29,6 @@ class StorageInterface(object): Interface of storage adapters for OctoPrint. """ - @property def analysis_backlog(self): """ @@ -155,7 +154,9 @@ def list_files(self, path=None, filter=None, recursive=True): def add_folder(self, path, ignore_existing=True): """ - Adds a folder as ``path``. The ``path`` will be sanitized. + Adds a folder as ``path`` + + The ``path`` will be sanitized. :param string path: the path of the new folder :param bool ignore_existing: if set to True, no error will be raised if the folder to be added already exists @@ -165,7 +166,7 @@ def add_folder(self, path, ignore_existing=True): def remove_folder(self, path, recursive=True): """ - Removes the folder at ``path``. + Removes the folder at ``path`` :param string path: the path of the folder to remove :param bool recursive: if set to True, contained folders and files will also be removed, otherwise and error will @@ -212,7 +213,9 @@ def add_file(self, path, file_object, printer_profile=None, links=None, allow_ov def remove_file(self, path): """ - Removes the file at ``path``. Will also take care of deleting the corresponding entries + Removes the file at ``path`` + + Will also take care of deleting the corresponding entries in the metadata and deleting all links pointing to the file. :param string path: path of the file to remove @@ -486,6 +489,9 @@ def analysis_backlog(self): return self.analysis_backlog_for_path() def analysis_backlog_for_path(self, path=None): + if path: + path = self.sanitize_path(path) + for entry in self._analysis_backlog_generator(path): yield entry @@ -663,16 +669,9 @@ def add_file(self, path, file_object, printer_profile=None, links=None, allow_ov # save the file's hash to the metadata of the folder file_hash = self._create_hash(file_path) metadata = self._get_metadata_entry(path, name, default=dict()) - metadata_dirty = False if not "hash" in metadata or metadata["hash"] != file_hash: - metadata["hash"] = file_hash - metadata_dirty = True - if "analysis" in metadata: - del metadata["analysis"] - metadata_dirty = True - - if metadata_dirty: - self._update_metadata_entry(path, name, metadata) + # hash changed -> throw away old metadata + self._update_metadata_entry(path, name, dict(hash=file_hash)) # process any links that were also provided for adding to the file if not links: diff --git a/src/octoprint/plugin/__init__.py b/src/octoprint/plugin/__init__.py index 31367e5422..57adfb2edf 100644 --- a/src/octoprint/plugin/__init__.py +++ b/src/octoprint/plugin/__init__.py @@ -239,7 +239,7 @@ class PluginSettings(object): """ The :class:`PluginSettings` class is the interface for plugins to their own or globally defined settings. - It provides a couple of convenience methods for directly accessing plugin settings via the regular + It provides some convenience methods for directly accessing plugin settings via the regular :class:`octoprint.settings.Settings` interfaces as well as means to access plugin specific folder locations. All getter and setter methods will ensure that plugin settings are stored in their correct location within the diff --git a/src/octoprint/plugin/core.py b/src/octoprint/plugin/core.py index 62da7633be..4f98a29abb 100644 --- a/src/octoprint/plugin/core.py +++ b/src/octoprint/plugin/core.py @@ -71,6 +71,9 @@ class PluginInfo(object): attr_description = '__plugin_description__' """ Module attribute from which to retrieve the plugin's description. """ + attr_disabling_discouraged = '__plugin_disabling_discouraged__' + """ Module attribute from which to retrieve the reason why disabling the plugin is discouraged. Only effective if ``self.bundled`` is True. """ + attr_version = '__plugin_version__' """ Module attribute from which to retrieve the plugin's version. """ @@ -294,6 +297,19 @@ def description(self): """ return self._get_instance_attribute(self.__class__.attr_description, default=self._description) + @property + def disabling_discouraged(self): + """ + Reason why disabling of this plugin is discouraged. Only evaluated for bundled plugins! Will be taken from + the disabling_discouraged attribute of the plugin module as defined in :attr:`attr_disabling_discouraged` if + available. False if unset or plugin not bundled. + + Returns: + str or None: Reason why disabling this plugin is discouraged (only for bundled plugins) + """ + return self._get_instance_attribute(self.__class__.attr_disabling_discouraged, default=False) if self.bundled \ + else False + @property def version(self): """ @@ -555,6 +571,10 @@ def _find_plugins_from_folders(self, folders, existing, ignored_uninstalled=True key = entry.name elif entry.is_file() and entry.name.endswith(".py"): key = entry.name[:-3] # strip off the .py extension + if key.startswith("__"): + # might be an __init__.py in our plugins folder, or something else we don't want + # to handle + continue else: continue diff --git a/src/octoprint/plugin/types.py b/src/octoprint/plugin/types.py index 2f856e3deb..098688f673 100644 --- a/src/octoprint/plugin/types.py +++ b/src/octoprint/plugin/types.py @@ -531,7 +531,7 @@ class UiPlugin(OctoPrintPlugin, SortablePlugin): provided request by calling :meth:`~octoprint.plugin.UiPlugin.will_handle_ui` with the Flask `Request `_ object as parameter. If you plugin returns `True` here, OctoPrint will next call - :meth:`~octoprint.plugin.UiPlugin.on_ui_render` with a couple of parameters like + :meth:`~octoprint.plugin.UiPlugin.on_ui_render` with a few parameters like - again - the Flask Request object and the render keyword arguments as used by the default OctoPrint web interface. For more information see below. @@ -582,7 +582,7 @@ class UiPlugin(OctoPrintPlugin, SortablePlugin): it only gets re-rendered if the request demands that (by having no-cache headers set) or if the cache gets invalidated otherwise. - In order to be able to do that, the ``UiPlugin`` offers overriding a couple of cache specific + In order to be able to do that, the ``UiPlugin`` offers overriding some cache specific methods used for figuring out the source files whose modification time to use for cache invalidation as well as override possibilities for ETag and LastModified calculation. Additionally there are methods to allow persisting call parameters to allow for preemptively caching your UI during @@ -1038,7 +1038,7 @@ class SimpleApiPlugin(OctoPrintPlugin): mixin offers. Use this mixin if all you need to do is return some kind of dynamic data to your plugin from the backend - and/or want to react to simple commands which boil down to a type of command and a couple of flat parameters + and/or want to react to simple commands which boil down to a type of command and a few flat parameters supplied with it. The simple API constructed by OctoPrint for you will be made available under ``/api/plugin//``. @@ -1377,6 +1377,14 @@ def on_after_startup(self): Of course, you are always free to completely override both :func:`on_settings_load` and :func:`on_settings_save` if the default implementations do not fit your requirements. + + .. warning:: + + Make sure to protect sensitive information stored by your plugin that only logged in administrators (or users) + should have access to via :meth:`~octoprint.plugin.SettingsPlugin.get_settings_restricted_paths`. OctoPrint will + return its settings on the REST API even to anonymous clients, but will filter out fields it know are restricted, + therefore you **must** make sure that you specify sensitive information accordingly to limit access as required! + .. attribute:: _settings The :class:`~octoprint.plugin.PluginSettings` instance to use for accessing the plugin's settings. Injected by @@ -1548,8 +1556,8 @@ def get_settings_defaults(self): field="field"), path=dict(to=dict(never=dict(return="return")))) - def get_settings_restricted_path(self): - return dict(admin=[["some", "admin_only", "path"], ["another", "admin_only", "path"], + def get_settings_restricted_paths(self): + return dict(admin=[["some", "admin_only", "path"], ["another", "admin_only", "path"],], user=[["some", "user_only", "path"],], never=[["path", "to", "never", "return"],]) diff --git a/src/octoprint/plugins/__init__.py b/src/octoprint/plugins/__init__.py new file mode 100644 index 0000000000..1b0510c60c --- /dev/null +++ b/src/octoprint/plugins/__init__.py @@ -0,0 +1 @@ +# make our plugins testable diff --git a/src/octoprint/plugins/announcements/__init__.py b/src/octoprint/plugins/announcements/__init__.py index 0ae7d9a51d..74bc192aef 100644 --- a/src/octoprint/plugins/announcements/__init__.py +++ b/src/octoprint/plugins/announcements/__init__.py @@ -18,6 +18,8 @@ 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 @@ -39,40 +41,75 @@ def __init__(self): # StartupPlugin def on_after_startup(self): - self._fetch_all_channels() + # decouple channel fetching from server startup + def fetch_data(): + self._fetch_all_channels() + + thread = threading.Thread(target=fetch_data) + thread.daemon = True + thread.start() # 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 +134,7 @@ def get_template_configs(self): 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 +155,17 @@ def view(): 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 +180,11 @@ def etag(): 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/", methods=["POST"]) @@ -208,9 +247,13 @@ def _get_channel_configs(self, force=False): 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 @@ -287,7 +330,8 @@ def _get_channel_data_from_network(self, key, config): url = config["url"] try: start = time.time() - r = requests.get(url) + r = requests.get(url, timeout=30) + r.raise_for_status() self._logger.info(u"Loaded channel {} from {} in {:.2}s".format(key, config["url"], time.time() - start)) except Exception as e: self._logger.exception( @@ -322,7 +366,7 @@ def _to_internal_entry(self, entry, read_until=None): return dict(title=entry["title"], title_without_tags=_strip_tags(entry["title"]), - summary=entry["summary"], + summary=_lazy_images(entry["summary"]), summary_without_images=_strip_images(entry["summary"]), published=published, link=entry["link"], @@ -337,8 +381,57 @@ def _get_channel_cache_path(self, key): _image_tag_re = re.compile(r'') def _strip_images(text): + """ + >>> _strip_images(u"I'm a link and this is an image: foo") + u"I'm a link and this is an image: " + >>> _strip_images(u"One and two and three and four \\"four\\"") + u'One and two and three and four ' + >>> _strip_images(u"No images here") + u'No images here' + """ return _image_tag_re.sub('', text) +def _replace_images(text, callback): + """ + >>> callback = lambda img: "foobar" + >>> _replace_images(u"I'm a link and this is an image: foo", callback) + u"I'm a link and this is an image: foobar" + >>> _replace_images(u"One and two and three and four \\"four\\"", callback) + u'One foobar and two foobar and three foobar and four foobar' + """ + result = text + for match in _image_tag_re.finditer(text): + tag = match.group(0) + replaced = callback(tag) + result = result.replace(tag, replaced) + return result + +_image_src_re = re.compile(r'src=(?P[\'"]*)(?P.*?)(?P=quote)(?=\s+|>)') +def _lazy_images(text, placeholder=None): + """ + >>> _lazy_images(u"I'm a link and this is an image: foo") + u'I\\'m a link and this is an image: \\'foo\\'' + >>> _lazy_images(u"I'm a link and this is an image: foo", placeholder="ph.png") + u'I\\'m a link and this is an image: \\'foo\\'' + >>> _lazy_images(u"One and two and three and four \\"four\\"", placeholder="ph.png") + u'One and two and three and four four' + >>> _lazy_images(u"No images here") + u'No images here' + """ + if placeholder is None: + # 1px transparent gif + placeholder = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" + + def callback(img_tag): + match = _image_src_re.search(img_tag) + if match is not None: + src = match.group("src") + quote = match.group("quote") + quoted_src = quote + src + quote + img_tag = img_tag.replace(match.group(0), 'src="{}" data-src={}'.format(placeholder, quoted_src)) + return img_tag + + return _replace_images(text, callback) def _strip_tags(text): """ @@ -374,4 +467,9 @@ def get_data(self): __plugin_name__ = "Announcement Plugin" +__plugin_author__ = "Gina Häußge" +__plugin_description__ = "Announcements all around OctoPrint" +__plugin_disabling_discouraged__ = gettext("Without this plugin you might miss important announcements " + "regarding security or other critical issues concerning OctoPrint.") +__plugin_license__ = "AGPLv3" __plugin_implementation__ = AnnouncementPlugin() diff --git a/src/octoprint/plugins/announcements/static/css/announcements.css b/src/octoprint/plugins/announcements/static/css/announcements.css index 08e9a9bd00..4df82b3961 100644 --- a/src/octoprint/plugins/announcements/static/css/announcements.css +++ b/src/octoprint/plugins/announcements/static/css/announcements.css @@ -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} \ No newline at end of file +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} \ 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 index 55c2081519..2fe4dbb7d4 100644 --- a/src/octoprint/plugins/announcements/static/js/announcements.js +++ b/src/octoprint/plugins/announcements/static/js/announcements.js @@ -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,11 @@ $(function() { }; self.showAnnouncementDialog = function(channel) { + if (!self.loginState.isAdmin()) return; + + // lazy load images that still need lazy-loading + $("#plugin_announcements_dialog_content article img").lazyload(); + self.announcementDialogContent.scrollTop(0); if (!self.announcementDialog.hasClass("in")) { @@ -172,6 +178,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 +230,7 @@ $(function() { } var rest = newItems.length - displayedItems.length; - var text = "
    "; + var text = "
      "; _.each(displayedItems, function(item) { var limitedSummary = stripParagraphs(item.summary_without_images.trim()); if (limitedSummary.length > maxLength) { @@ -239,6 +247,8 @@ $(function() { text += gettext(_.sprintf("... and %(rest)d more.", {rest: rest})); } + text += "" + gettext("You can edit your announcement subscriptions under Settings > Announcements.") + ""; + var options = { title: channel, text: text, @@ -284,10 +294,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"); diff --git a/src/octoprint/plugins/announcements/static/less/announcements.less b/src/octoprint/plugins/announcements/static/less/announcements.less index 1f57d796ce..9e90b346c7 100644 --- a/src/octoprint/plugins/announcements/static/less/announcements.less +++ b/src/octoprint/plugins/announcements/static/less/announcements.less @@ -53,4 +53,10 @@ table { } } } + + .modal-footer { + .configurelink { + float: left; + } + } } diff --git a/src/octoprint/plugins/announcements/templates/announcements.jinja2 b/src/octoprint/plugins/announcements/templates/announcements.jinja2 index 6c565254d6..24fbe551f6 100644 --- a/src/octoprint/plugins/announcements/templates/announcements.jinja2 +++ b/src/octoprint/plugins/announcements/templates/announcements.jinja2 @@ -11,7 +11,7 @@
    • - +
    • @@ -40,6 +40,7 @@ diff --git a/src/octoprint/plugins/announcements/templates/announcements_settings.jinja2 b/src/octoprint/plugins/announcements/templates/announcements_settings.jinja2 index 3d62eeb3af..0d76029c19 100644 --- a/src/octoprint/plugins/announcements/templates/announcements_settings.jinja2 +++ b/src/octoprint/plugins/announcements/templates/announcements_settings.jinja2 @@ -10,7 +10,8 @@ -
      +
      +
       
      diff --git a/src/octoprint/plugins/corewizard/__init__.py b/src/octoprint/plugins/corewizard/__init__.py index 7ce014be0a..d9c61f67d2 100644 --- a/src/octoprint/plugins/corewizard/__init__.py +++ b/src/octoprint/plugins/corewizard/__init__.py @@ -172,5 +172,9 @@ def _get_subwizard_attrs(self, start, end, callback=None): __plugin_name__ = "Core Wizard" +__plugin_author__ = "Gina Häußge" __plugin_description__ = "Provides wizard dialogs for core components and functionality" +__plugin_disabling_discouraged__ = gettext("Without this plugin OctoPrint will no longer be able to perform " + "setup steps that might be required after an update.") +__plugin_license__ = "AGPLv3" __plugin_implementation__ = CoreWizardPlugin() diff --git a/src/octoprint/plugins/cura/__init__.py b/src/octoprint/plugins/cura/__init__.py index 058445d650..8713c37789 100644 --- a/src/octoprint/plugins/cura/__init__.py +++ b/src/octoprint/plugins/cura/__init__.py @@ -17,6 +17,7 @@ import octoprint.settings from octoprint.util.paths import normalize as normalize_path +from octoprint.util import to_unicode from .profile import Profile from .profile import GcodeFlavors @@ -236,7 +237,8 @@ def save_slicer_profile(self, path, profile, allow_overwrite=True, overrides=Non self._save_profile(path, new_profile, allow_overwrite=allow_overwrite) - def do_slice(self, model_path, printer_profile, machinecode_path=None, profile_path=None, position=None, on_progress=None, on_progress_args=None, on_progress_kwargs=None): + def do_slice(self, model_path, printer_profile, machinecode_path=None, profile_path=None, position=None, + on_progress=None, on_progress_args=None, on_progress_kwargs=None): try: with self._job_mutex: if not profile_path: @@ -258,7 +260,10 @@ def do_slice(self, model_path, printer_profile, machinecode_path=None, profile_p if not on_progress_kwargs: on_progress_kwargs = dict() - self._cura_logger.info(u"### Slicing %s to %s using profile stored at %s" % (model_path, machinecode_path, profile_path)) + self._cura_logger.info(u"### Slicing {} to {} using profile stored at {}" + .format(to_unicode(model_path, errors="replace"), + to_unicode(machinecode_path, errors="replace"), + to_unicode(profile_path, errors="replace"))) executable = normalize_path(self._settings.get(["cura_engine"])) if not executable: @@ -293,7 +298,9 @@ def do_slice(self, model_path, printer_profile, machinecode_path=None, profile_p args += ["-s", "%s=%s" % (k, str(v))] args += ["-o", machinecode_path, model_path] - self._logger.info(u"Running %r in %s" % (" ".join(args), working_dir)) + self._logger.info(u"Running {!r} in {}".format(u" ".join(map(lambda x: to_unicode(x, errors="replace"), + args)), + working_dir)) import sarge p = sarge.run(args, cwd=working_dir, async=True, stdout=sarge.Capture(), stderr=sarge.Capture()) @@ -314,7 +321,7 @@ def do_slice(self, model_path, printer_profile, machinecode_path=None, profile_p p.commands[0].poll() continue - line = octoprint.util.to_unicode(line, errors="replace") + line = to_unicode(line, errors="replace") self._cura_logger.debug(line.strip()) if on_progress is not None: @@ -437,7 +444,8 @@ def cancel_slicing(self, machinecode_path): command = self._slicing_commands[machinecode_path] if command is not None: command.terminate() - self._logger.info(u"Cancelled slicing of %s" % machinecode_path) + self._logger.info(u"Cancelled slicing of {}" + .format(to_unicode(machinecode_path, errors="replace"))) def _load_profile(self, path): import yaml @@ -505,7 +513,7 @@ def _get_usage_from_length(filament_length, filament_diameter): __plugin_name__ = "CuraEngine (<= 15.04)" __plugin_author__ = "Gina Häußge" -__plugin_url__ = "https://github.com/foosel/OctoPrint/wiki/Plugin:-Cura" +__plugin_url__ = "http://docs.octoprint.org/en/master/bundledplugins/cura.html" __plugin_description__ = "Adds support for slicing via CuraEngine versions up to and including version 15.04 from within OctoPrint" __plugin_license__ = "AGPLv3" __plugin_implementation__ = CuraPlugin() diff --git a/src/octoprint/plugins/cura/static/js/cura.js b/src/octoprint/plugins/cura/static/js/cura.js index 4b57e623bd..87f9d5f640 100644 --- a/src/octoprint/plugins/cura/static/js/cura.js +++ b/src/octoprint/plugins/cura/static/js/cura.js @@ -25,11 +25,35 @@ $(function() { self.profileAllowOverwrite = ko.observable(true); self.profileMakeDefault = ko.observable(false); + // make sure to update form data if any of the metadata changes + self.profileName.subscribe(function() { self.copyProfileMetadata(); }); + self.profileDisplayName.subscribe(function() { + if (self.profileDisplayName()) { + self.placeholderName(self._sanitize(self.profileDisplayName()).toLowerCase()); + } + self.copyProfileMetadata(); + }); + self.profileDescription.subscribe(function() { self.copyProfileMetadata(); }); + self.profileAllowOverwrite.subscribe(function() { self.copyProfileMetadata(); }); + self.profileMakeDefault.subscribe(function() { self.copyProfileMetadata(); }); + self.unconfiguredCuraEngine = ko.observable(); self.unconfiguredSlicingProfile = ko.observable(); + self.uploadDialog = $("#settings_plugin_cura_import"); self.uploadElement = $("#settings-cura-import"); - self.uploadButton = $("#settings-cura-import-start"); + self.uploadData = ko.observable(undefined); + self.uploadBusy = ko.observable(false); + + self.uploadEnabled = ko.pureComputed(function() { + return self.fieldsEnabled(); + }); + self.fieldsEnabled = ko.pureComputed(function() { + return self.uploadData() && !self.uploadBusy() + && (self.profileName() || self.placeholderName()) + && (self.profileDisplayName() || self.placeholderDisplayName()) + && (self.profileDescription() || self.placeholderDescription()); + }); self.profiles = new ItemListHelper( "plugin_cura_profiles", @@ -66,6 +90,53 @@ $(function() { return name.replace(/[^a-zA-Z0-9\-_\.\(\) ]/g, "").replace(/ /g, "_"); }; + self.performUpload = function() { + if (self.uploadData()) { + self.uploadData().submit(); + } + }; + + self.copyProfileMetadata = function(form) { + form = form || (self.uploadData() ? self.uploadData().formData : {}); + + if (self.profileName() !== undefined) { + form["name"] = self.profileName(); + } else if (self.placeholderName() !== undefined) { + form["name"] = self.placeholderName(); + } + + if (self.profileDisplayName() !== undefined) { + form["displayName"] = self.profileDisplayName(); + } else if (self.placeholderDisplayName() !== undefined) { + form["displayName"] = self.placeholderDisplayName(); + } + + if (self.profileDescription() !== undefined) { + form["description"] = self.profileDescription(); + } else if (self.placeholderDescription() !== undefined) { + form["description"] = self.placeholderDescription(); + } + + if (self.profileMakeDefault()) { + form["default"] = true; + } + + return form; + }; + + self.clearUpload = function() { + self.uploadData(undefined); + self.fileName(undefined); + self.placeholderName(undefined); + self.placeholderDisplayName(undefined); + self.placeholderDescription(undefined); + self.profileName(undefined); + self.profileDisplayName(undefined); + self.profileDescription(undefined); + self.profileAllowOverwrite(true); + self.profileMakeDefault(false); + }; + self.uploadElement.fileupload({ dataType: "json", maxNumberOfFiles: 1, @@ -73,6 +144,11 @@ $(function() { headers: OctoPrint.getRequestHeaders(), add: function(e, data) { if (data.files.length == 0) { + // no files? ignore + return false; + } + if (self.uploadData()) { + // data already defined? ignore (should never happen) return false; } @@ -83,41 +159,22 @@ $(function() { self.placeholderDisplayName(name); self.placeholderDescription("Imported from " + self.fileName() + " on " + formatDate(new Date().getTime() / 1000)); - self.uploadButton.unbind("click"); - self.uploadButton.on("click", function() { - var form = { - allowOverwrite: self.profileAllowOverwrite() - }; - - if (self.profileName() !== undefined) { - form["name"] = self.profileName(); - } - if (self.profileDisplayName() !== undefined) { - form["displayName"] = self.profileDisplayName(); - } - if (self.profileDescription() !== undefined) { - form["description"] = self.profileDescription(); - } - if (self.profileMakeDefault()) { - form["default"] = true; - } + var form = { + allowOverwrite: self.profileAllowOverwrite() + }; + data.formData = self.copyProfileMetadata(form); - data.formData = form; - data.submit(); - }); + self.uploadData(data); + }, + submit: function(e, data) { + self.copyProfileMetadata(); + self.uploadBusy(true); }, done: function(e, data) { - self.fileName(undefined); - self.placeholderName(undefined); - self.placeholderDisplayName(undefined); - self.placeholderDescription(undefined); - self.profileName(undefined); - self.profileDisplayName(undefined); - self.profileDescription(undefined); - self.profileAllowOverwrite(true); - self.profileMakeDefault(false); - - $("#settings_plugin_cura_import").modal("hide"); + self.uploadBusy(false); + self.clearUpload(); + + self.uploadDialog.modal("hide"); self.requestData(); self.slicingViewModel.requestData(); } @@ -160,12 +217,9 @@ $(function() { }); }; - self.showImportProfileDialog = function(makeDefault) { - if (makeDefault == undefined) { - makeDefault = _.filter(self.profiles.items(), function(profile) { profile.isdefault() }).length == 0; - } - self.profileMakeDefault(makeDefault); - $("#settings_plugin_cura_import").modal("show"); + self.showImportProfileDialog = function() { + self.clearUpload(); + self.uploadDialog.modal("show"); }; self.testEnginePath = function() { @@ -207,7 +261,18 @@ $(function() { self.onBeforeBinding = function () { self.settings = self.settingsViewModel.settings; - //self.requestData(); + }; + + self.onAllBound = function() { + self.uploadDialog.on("hidden", function(event) { + if (event.target.id == "settings_plugin_cura_import") { + self.clearUpload(); + } + }); + }; + + self.onSettingsShown = function() { + self.requestData(); }; self.onSettingsHidden = function() { diff --git a/src/octoprint/plugins/cura/templates/snippets/settings/cura/profileImporter.jinja2 b/src/octoprint/plugins/cura/templates/snippets/settings/cura/profileImporter.jinja2 index 063fd5b7c8..7057bff274 100644 --- a/src/octoprint/plugins/cura/templates/snippets/settings/cura/profileImporter.jinja2 +++ b/src/octoprint/plugins/cura/templates/snippets/settings/cura/profileImporter.jinja2 @@ -20,31 +20,31 @@
      - +
      - +
      - +
      -
      -
      @@ -59,6 +59,6 @@
      diff --git a/src/octoprint/plugins/discovery/__init__.py b/src/octoprint/plugins/discovery/__init__.py index 7e055a27e6..988d4840fa 100644 --- a/src/octoprint/plugins/discovery/__init__.py +++ b/src/octoprint/plugins/discovery/__init__.py @@ -12,6 +12,7 @@ import logging import os import flask +from flask.ext.babel import gettext from builtins import range import octoprint.plugin @@ -25,8 +26,10 @@ __plugin_name__ = "Discovery" __plugin_author__ = "Gina Häußge" -__plugin_url__ = "https://github.com/foosel/OctoPrint/wiki/Plugin:-Discovery" +__plugin_url__ = "http://docs.octoprint.org/en/master/bundledplugins/discovery.html" __plugin_description__ = "Makes the OctoPrint instance discoverable via Bonjour/Avahi/Zeroconf and uPnP" +__plugin_disabling_discouraged__ = gettext("Without this plugin your OctoPrint instance will no longer be " + "discoverable on the network via Bonjour and uPnP.") __plugin_license__ = "AGPLv3" def __plugin_load__(): diff --git a/src/octoprint/plugins/pluginmanager/__init__.py b/src/octoprint/plugins/pluginmanager/__init__.py index 906055178e..47365b2e8e 100644 --- a/src/octoprint/plugins/pluginmanager/__init__.py +++ b/src/octoprint/plugins/pluginmanager/__init__.py @@ -16,6 +16,7 @@ from flask import jsonify, make_response from flask.ext.babel import gettext +from collections import OrderedDict import logging import sarge @@ -24,6 +25,10 @@ import re import os import pkg_resources +import copy +import dateutil.parser +import time +import threading class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin, octoprint.plugin.TemplatePlugin, @@ -35,8 +40,9 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin, ARCHIVE_EXTENSIONS = (".zip", ".tar.gz", ".tgz", ".tar") OPERATING_SYSTEMS = dict(windows=["win32"], - linux=["linux2"], - macos=["darwin"]) + linux=lambda x: x.startswith("linux"), + macos=["darwin"], + freebsd=lambda x: x.startswith("freebsd")) pip_inapplicable_arguments = dict(uninstall=["--user"]) @@ -53,12 +59,19 @@ def __init__(self): self._repository_cache_path = None self._repository_cache_ttl = 0 + self._notices = dict() + self._notices_available = False + self._notices_cache_path = None + self._notices_cache_ttl = 0 + self._console_logger = None def initialize(self): self._console_logger = logging.getLogger("octoprint.plugins.pluginmanager.console") self._repository_cache_path = os.path.join(self.get_plugin_data_folder(), "plugins.json") self._repository_cache_ttl = self._settings.get_int(["repository_ttl"]) * 60 + self._notices_cache_path = os.path.join(self.get_plugin_data_folder(), "notices.json") + self._notices_cache_ttl = self._settings.get_int(["notices_ttl"]) * 60 self._pip_caller = LocalPipCaller(force_user=self._settings.get_boolean(["pip_force_user"])) self._pip_caller.on_log_call = self._log_call @@ -73,7 +86,7 @@ def increase_upload_bodysize(self, current_max_body_sizes, *args, **kwargs): ##~~ StartupPlugin - def on_startup(self, host, port): + def on_after_startup(self): from octoprint.logging.handlers import CleaningTimedRotatingFileHandler console_logging_handler = CleaningTimedRotatingFileHandler(self._settings.get_plugin_logfile_path(postfix="console"), when="D", backupCount=3) console_logging_handler.setFormatter(logging.Formatter("%(asctime)s %(message)s")) @@ -83,7 +96,14 @@ def on_startup(self, host, port): self._console_logger.setLevel(logging.DEBUG) self._console_logger.propagate = False - self._repository_available = self._fetch_repository_from_disk() + # decouple repository fetching from server startup + def fetch_data(): + self._repository_available = self._fetch_repository_from_disk() + self._notices_available = self._fetch_notices_from_disk() + + thread = threading.Thread(target=fetch_data) + thread.daemon = True + thread.start() ##~~ SettingsPlugin @@ -91,6 +111,8 @@ def get_settings_defaults(self): return dict( repository="http://plugins.octoprint.org/plugins.json", repository_ttl=24*60, + notices="http://plugins.octoprint.org/notices.json", + notices_ttl=6*60, pip_args=None, pip_force_user=False, dependency_links=False, @@ -101,6 +123,7 @@ def on_settings_save(self, data): octoprint.plugin.SettingsPlugin.on_settings_save(self, data) self._repository_cache_ttl = self._settings.get_int(["repository_ttl"]) * 60 + self._notices_cache_ttl = self._settings.get_int(["notices_ttl"]) * 60 self._pip_caller.force_user = self._settings.get_boolean(["pip_force_user"]) ##~~ AssetPlugin @@ -192,6 +215,10 @@ def on_api_get(self, request): if refresh_repository: self._repository_available = self._refresh_repository() + refresh_notices = request.values.get("refresh_notices", "false") in valid_boolean_trues + if refresh_notices: + self._notices_available = self._refresh_notices() + def view(): return jsonify(plugins=self._get_plugins(), repository=dict( @@ -217,6 +244,8 @@ def etag(): hash.update(repr(self._get_plugins())) hash.update(str(self._repository_available)) hash.update(repr(self._repository_plugins)) + hash.update(str(self._notices_available)) + hash.update(repr(self._notices)) hash.update(repr(safe_mode)) return hash.hexdigest() @@ -225,7 +254,7 @@ def condition(): return with_revalidation_checking(etag_factory=lambda *args, **kwargs: etag(), condition=lambda *args, **kwargs: condition(), - unless=lambda: refresh_repository)(view)() + unless=lambda: refresh_repository or refresh_notices)(view)() def on_api_command(self, command, data): if not admin_permission.can(): @@ -240,7 +269,8 @@ def on_api_command(self, command, data): plugin_name = data["plugin"] if "plugin" in data else None return self.command_install(url=url, force="force" in data and data["force"] in valid_boolean_trues, - dependency_links="dependency_links" in data and data["dependency_links"] in valid_boolean_trues, + dependency_links="dependency_links" in data + and data["dependency_links"] in valid_boolean_trues, reinstall=plugin_name) elif command == "uninstall": @@ -261,37 +291,84 @@ def on_api_command(self, command, data): def command_install(self, url=None, path=None, force=False, reinstall=None, dependency_links=False): if url is not None: - pip_args = ["install", sarge.shell_quote(url)] + source = url + source_type = "url" + already_installed_check = lambda line: url in line + elif path is not None: - pip_args = ["install", sarge.shell_quote(path)] + path = os.path.abspath(path) + path_url = "file://" + path + if os.sep != "/": + # windows gets special handling + path = path.replace(os.sep, "/").lower() + path_url = "file:///" + path + + source = path + source_type = "path" + already_installed_check = lambda line: path_url in line.lower() # lower case in case of windows + else: raise ValueError("Either URL or path must be provided") + self._logger.info("Installing plugin from {}".format(source)) + pip_args = ["install", sarge.shell_quote(source)] + if dependency_links or self._settings.get_boolean(["dependency_links"]): pip_args.append("--process-dependency-links") - all_plugins_before = self._plugin_manager.find_plugins() + all_plugins_before = self._plugin_manager.find_plugins(existing=dict()) + already_installed_string = "Requirement already satisfied (use --upgrade to upgrade)" success_string = "Successfully installed" failure_string = "Could not install" + try: returncode, stdout, stderr = self._call_pip(pip_args) + + # pip's output for a package that is already installed looks something like any of these: + # + # Requirement already satisfied (use --upgrade to upgrade): OctoPrint-Plugin==1.0 from \ + # https://example.com/foobar.zip in + # Requirement already satisfied (use --upgrade to upgrade): OctoPrint-Plugin in + # Requirement already satisfied (use --upgrade to upgrade): OctoPrint-Plugin==1.0 from \ + # file:///tmp/foobar.zip in + # Requirement already satisfied (use --upgrade to upgrade): OctoPrint-Plugin==1.0 from \ + # file:///C:/Temp/foobar.zip in + # + # If we detect any of these matching what we just tried to install, we'll need to trigger a second + # install with reinstall flags. + + if not force and any(map(lambda x: x.strip().startswith(already_installed_string) and already_installed_check(x), + stdout)): + self._logger.info("Plugin to be installed from {} was already installed, forcing a reinstall".format(source)) + self._log_message("Looks like the plugin was already installed. Forcing a reinstall.") + force = True except: self._logger.exception("Could not install plugin from %s" % url) return make_response("Could not install plugin from URL, see the log for more details", 500) else: if force: + # We don't use --upgrade here because that will also happily update all our dependencies - we'd rather + # do that in a controlled manner pip_args += ["--ignore-installed", "--force-reinstall", "--no-deps"] try: returncode, stdout, stderr = self._call_pip(pip_args) except: - self._logger.exception("Could not install plugin from %s" % url) - return make_response("Could not install plugin from URL, see the log for more details", 500) + self._logger.exception("Could not install plugin from {}".format(source)) + return make_response("Could not install plugin from source {}, see the log for more details" + .format(source), 500) try: - result_line = filter(lambda x: x.startswith(success_string) or x.startswith(failure_string), stdout)[-1] + result_line = filter(lambda x: x.startswith(success_string) or x.startswith(failure_string), + stdout)[-1] except IndexError: - result = dict(result=False, reason="Could not parse output from pip") + self._logger.error("Installing the plugin from {} failed, could not parse output from pip. " + "See plugin_pluginmanager_console.log for generated output".format(source)) + result = dict(result=False, + source=source, + source_type=source_type, + reason="Could not parse output from pip, see plugin_pluginmanager_console.log " + "for generated output") self._send_result_notification("install", result) return jsonify(result) @@ -304,8 +381,8 @@ def command_install(self, url=None, path=None, force=False, reinstall=None, depe # Successfully installed OctoPrint-Plugin Dependency-One Dependency-Two # Cleaning up... # - # So we'll need to fetch the "Successfully installed" line, strip the "Successfully" part, then split by whitespace - # and strip to get all installed packages. + # So we'll need to fetch the "Successfully installed" line, strip the "Successfully" part, then split + # by whitespace and strip to get all installed packages. # # We then need to iterate over all known plugins and see if either the package name or the package name plus # version number matches one of our installed packages. If it does, that's our installed plugin. @@ -317,58 +394,54 @@ def command_install(self, url=None, path=None, force=False, reinstall=None, depe result_line = result_line.strip() if not result_line.startswith(success_string): - result = dict(result=False, reason="Pip did not report successful installation") + self._logger.error("Installing the plugin from {} failed, pip did not report successful installation" + .format(source)) + result = dict(result=False, + source=source, + source_type=source_type, + reason="Pip did not report successful installation") self._send_result_notification("install", result) return jsonify(result) installed = map(lambda x: x.strip(), result_line[len(success_string):].split(" ")) all_plugins_after = self._plugin_manager.find_plugins(existing=dict(), ignore_uninstalled=False) - for key, plugin in all_plugins_after.items(): - if plugin.origin is None or plugin.origin.type != "entry_point": - continue - - package_name = plugin.origin.package_name - package_version = plugin.origin.package_version - versioned_package = "{package_name}-{package_version}".format(**locals()) - - if package_name in installed or versioned_package in installed: - # exact match, we are done here - new_plugin_key = key - new_plugin = plugin - break - - else: - # it might still be a version that got stripped by python's package resources, e.g. 1.4.5a0 => 1.4.5a - found = False - - for inst in installed: - if inst.startswith(versioned_package): - found = True - break - - if found: - new_plugin_key = key - new_plugin = plugin - break - else: - self._logger.warn("The plugin was installed successfully, but couldn't be found afterwards to initialize properly during runtime. Please restart OctoPrint.") - result = dict(result=True, url=url, needs_restart=True, needs_refresh=True, was_reinstalled=False, plugin="unknown") + new_plugin = self._find_installed_plugin(installed, plugins=all_plugins_after) + if new_plugin is None: + self._logger.warn("The plugin was installed successfully, but couldn't be found afterwards to " + "initialize properly during runtime. Please restart OctoPrint.") + result = dict(result=True, + source=source, + source_type=source_type, + needs_restart=True, + needs_refresh=True, + was_reinstalled=False, + plugin="unknown") self._send_result_notification("install", result) return jsonify(result) self._plugin_manager.reload_plugins() - needs_restart = self._plugin_manager.is_restart_needing_plugin(new_plugin) or new_plugin_key in all_plugins_before or reinstall is not None - needs_refresh = new_plugin.implementation and isinstance(new_plugin.implementation, octoprint.plugin.ReloadNeedingPlugin) - - is_reinstall = self._plugin_manager.is_plugin_marked(new_plugin_key, "uninstalled") - self._plugin_manager.mark_plugin(new_plugin_key, + needs_restart = self._plugin_manager.is_restart_needing_plugin(new_plugin) \ + or new_plugin.key in all_plugins_before \ + or reinstall is not None + needs_refresh = new_plugin.implementation \ + and isinstance(new_plugin.implementation, octoprint.plugin.ReloadNeedingPlugin) + + is_reinstall = self._plugin_manager.is_plugin_marked(new_plugin.key, "uninstalled") + self._plugin_manager.mark_plugin(new_plugin.key, uninstalled=False, installed=not is_reinstall and needs_restart) self._plugin_manager.log_all_plugins() - result = dict(result=True, url=url, needs_restart=needs_restart, needs_refresh=needs_refresh, was_reinstalled=new_plugin_key in all_plugins_before or reinstall is not None, plugin=self._to_external_representation(new_plugin)) + self._logger.info("The plugin was installed successfully: {}, version {}".format(new_plugin.name, new_plugin.version)) + result = dict(result=True, + source=source, + source_type=source_type, + needs_restart=needs_restart, + needs_refresh=needs_refresh, + was_reinstalled=new_plugin.key in all_plugins_before or reinstall is not None, + plugin=self._to_external_plugin(new_plugin)) self._send_result_notification("install", result) return jsonify(result) @@ -448,7 +521,7 @@ def command_uninstall(self, plugin): self._plugin_manager.reload_plugins() - result = dict(result=True, needs_restart=needs_restart, needs_refresh=needs_refresh, plugin=self._to_external_representation(plugin)) + result = dict(result=True, needs_restart=needs_restart, needs_refresh=needs_refresh, plugin=self._to_external_plugin(plugin)) self._send_result_notification("uninstall", result) return jsonify(result) @@ -473,13 +546,49 @@ def command_toggle(self, plugin, command): self._logger.exception(u"Problem toggling enabled state of {name}: {reason}".format(name=plugin.key, reason=e.reason)) result = dict(result=False, reason=e.reason) except octoprint.plugin.core.PluginNeedsRestart: - result = dict(result=True, needs_restart=True, needs_refresh=True, plugin=self._to_external_representation(plugin)) + result = dict(result=True, + needs_restart=True, + needs_refresh=True, + plugin=self._to_external_plugin(plugin)) else: - result = dict(result=True, needs_restart=needs_restart_api, needs_refresh=needs_refresh_api, plugin=self._to_external_representation(plugin)) + result = dict(result=True, + needs_restart=needs_restart_api, + needs_refresh=needs_refresh_api, + plugin=self._to_external_plugin(plugin)) self._send_result_notification(command, result) return jsonify(result) + def _find_installed_plugin(self, packages, plugins=None): + if plugins is None: + plugins = self._plugin_manager.find_plugins(existing=dict(), ignore_uninstalled=False) + + for key, plugin in plugins.items(): + if plugin.origin is None or plugin.origin.type != "entry_point": + continue + + package_name = plugin.origin.package_name + package_version = plugin.origin.package_version + versioned_package = "{package_name}-{package_version}".format(**locals()) + + if package_name in packages or versioned_package in packages: + # exact match, we are done here + return plugin + + else: + # it might still be a version that got stripped by python's package resources, e.g. 1.4.5a0 => 1.4.5a + found = False + + for inst in packages: + if inst.startswith(versioned_package): + found = True + break + + if found: + return plugin + + return None + def _send_result_notification(self, action, result): notification = dict(type="result", action=action) notification.update(result) @@ -572,10 +681,10 @@ def _fetch_repository_from_disk(self): return self._refresh_repository(repo_data=repo_data) def _fetch_repository_from_url(self): - import requests repository_url = self._settings.get(["repository"]) try: - r = requests.get(repository_url) + r = requests.get(repository_url, timeout=30) + r.raise_for_status() self._logger.info("Loaded plugin repository data from {}".format(repository_url)) except Exception as e: self._logger.exception("Could not fetch plugins from repository at {repository_url}: {message}".format(repository_url=repository_url, message=str(e))) @@ -602,7 +711,7 @@ def _refresh_repository(self, repo_data=None): octoprint_version = self._get_octoprint_version(base=True) def map_repository_entry(entry): - result = dict(entry) + result = copy.deepcopy(entry) if not "follow_dependency_links" in result: result["follow_dependency_links"] = False @@ -624,6 +733,70 @@ def map_repository_entry(entry): self._repository_plugins = map(map_repository_entry, repo_data) return True + def _fetch_notices_from_disk(self): + notice_data = None + if os.path.isfile(self._notices_cache_path): + import time + mtime = os.path.getmtime(self._notices_cache_path) + if mtime + self._notices_cache_ttl >= time.time() > mtime: + try: + import json + with open(self._notices_cache_path) as f: + notice_data = json.load(f) + self._logger.info("Loaded notices from disk, was still valid") + except: + self._logger.exception("Error while loading notices from {}".format(self._notices_cache_path)) + + return self._refresh_notices(notice_data=notice_data) + + def _fetch_notices_from_url(self): + notices_url = self._settings.get(["notices"]) + try: + r = requests.get(notices_url, timeout=30) + r.raise_for_status() + self._logger.info("Loaded plugin notices data from {}".format(notices_url)) + except Exception as e: + self._logger.exception("Could not fetch notices from {notices_url}: {message}".format(notices_url=notices_url, message=str(e))) + return None + + notice_data = r.json() + + try: + import json + with octoprint.util.atomic_write(self._notices_cache_path, "wb") as f: + json.dump(notice_data, f) + except Exception as e: + self._logger.exception("Error while saving notices to {}: {}".format(self._notices_cache_path, str(e))) + return notice_data + + def _refresh_notices(self, notice_data=None): + if notice_data is None: + notice_data = self._fetch_notices_from_url() + if notice_data is None: + return False + + notices = dict() + for notice in notice_data: + if not "plugin" in notice or not "text" in notice or not "date" in notice: + continue + + key = notice["plugin"] + + try: + parsed_date = dateutil.parser.parse(notice["date"]) + notice["timestamp"] = parsed_date.timetuple() + except Exception as e: + self._logger.warn("Error while parsing date {!r} for plugin notice " + "of plugin {}, ignoring notice: {}".format(notice["date"], key, str(e))) + continue + + if not key in notices: + notices[key] = [] + notices[key].append(notice) + + self._notices = notices + return True + def _is_octoprint_compatible(self, octoprint_version, compatibility_entries): """ Tests if the current ``octoprint_version`` is compatible to any of the provided ``compatibility_entries``. @@ -644,18 +817,37 @@ def _is_octoprint_compatible(self, octoprint_version, compatibility_entries): return True - def _is_os_compatible(self, current_os, compatibility_entries): + @staticmethod + def _is_os_compatible(current_os, compatibility_entries): """ - Tests if the ``current_os`` matches any of the provided ``compatibility_entries``. + Tests if the ``current_os`` or ``sys.platform`` are blacklisted or whitelisted in ``compatibility_entries`` """ - return current_os in filter(lambda x: x in self.__class__.OPERATING_SYSTEMS.keys(), compatibility_entries) + if len(compatibility_entries) == 0: + # shortcut - no compatibility info means we are compatible + return True + + negative_entries = map(lambda x: x[1:], filter(lambda x: x.startswith("!"), compatibility_entries)) + positive_entries = filter(lambda x: not x.startswith("!"), compatibility_entries) - def _get_os(self): - for identifier, platforms in self.__class__.OPERATING_SYSTEMS.items(): - if sys.platform in platforms: + negative_match = False + if negative_entries: + # check if we are blacklisted + negative_match = current_os in negative_entries or any(map(lambda x: sys.platform.startswith(x), negative_entries)) + + positive_match = True + if positive_entries: + # check if we are whitelisted + positive_match = current_os in positive_entries or any(map(lambda x: sys.platform.startswith(x), positive_entries)) + + return positive_match and not negative_match + + @classmethod + def _get_os(cls): + for identifier, platforms in cls.OPERATING_SYSTEMS.items(): + if (callable(platforms) and platforms(sys.platform)) or (isinstance(platforms, list) and sys.platform in platforms): return identifier else: - return "unknown" + return "unmapped" def _get_octoprint_version_string(self): return VERSION @@ -694,18 +886,19 @@ def _get_plugins(self): hidden = self._settings.get(["hidden"]) result = [] - for name, plugin in plugins.items(): - if name in hidden: + for key, plugin in plugins.items(): + if key in hidden: continue - result.append(self._to_external_representation(plugin)) + result.append(self._to_external_plugin(plugin)) return result - def _to_external_representation(self, plugin): + def _to_external_plugin(self, plugin): return dict( key=plugin.key, name=plugin.name, description=plugin.description, + disabling_discouraged=gettext(plugin.disabling_discouraged) if plugin.disabling_discouraged else False, author=plugin.author, version=plugin.version, url=plugin.url, @@ -719,12 +912,45 @@ def _to_external_representation(self, plugin): pending_disable=((plugin.enabled or getattr(plugin, "safe_mode_enabled", False)) and plugin.key in self._pending_disable), pending_install=(self._plugin_manager.is_plugin_marked(plugin.key, "installed")), pending_uninstall=(self._plugin_manager.is_plugin_marked(plugin.key, "uninstalled")), - origin=plugin.origin.type + origin=plugin.origin.type, + notifications = self._get_notifications(plugin) ) + def _get_notifications(self, plugin): + key = plugin.key + if not plugin.enabled: + return + + if key not in self._notices: + return + + octoprint_version = self._get_octoprint_version(base=True) + plugin_notifications = self._notices.get(key, []) + + def filter_relevant(notification): + return "text" in notification and "date" in notification and \ + ("versions" not in notification or plugin.version in notification["versions"]) and \ + ("octoversions" not in notification or self._is_octoprint_compatible(octoprint_version, notification["octoversions"])) + + def map_notification(notification): + return self._to_external_notification(key, notification) + + return filter(lambda x: x is not None, + map(map_notification, + filter(filter_relevant, + plugin_notifications))) + + def _to_external_notification(self, key, notification): + return dict(key=key, + date=time.mktime(notification["timestamp"]), + text=notification["text"], + link=notification.get("link"), + versions=notification.get("versions", []), + important=notification.get("important", False)) + __plugin_name__ = "Plugin Manager" __plugin_author__ = "Gina Häußge" -__plugin_url__ = "https://github.com/foosel/OctoPrint/wiki/Plugin:-Plugin-Manager" +__plugin_url__ = "http://docs.octoprint.org/en/master/bundledplugins/pluginmanager.html" __plugin_description__ = "Allows installing and managing OctoPrint plugins" __plugin_license__ = "AGPLv3" diff --git a/src/octoprint/plugins/pluginmanager/static/css/pluginmanager.css b/src/octoprint/plugins/pluginmanager/static/css/pluginmanager.css index b1cbc9af77..65ae4796cc 100644 --- a/src/octoprint/plugins/pluginmanager/static/css/pluginmanager.css +++ b/src/octoprint/plugins/pluginmanager/static/css/pluginmanager.css @@ -1 +1 @@ -table th.settings_plugin_plugin_manager_plugins_name,table td.settings_plugin_plugin_manager_plugins_name{text-overflow:ellipsis;text-align:left}table th.settings_plugin_plugin_manager_plugins_actions,table td.settings_plugin_plugin_manager_plugins_actions{text-align:center;width:80px}table th.settings_plugin_plugin_manager_plugins_actions a,table td.settings_plugin_plugin_manager_plugins_actions a{text-decoration:none;color:#000}table th.settings_plugin_plugin_manager_plugins_actions a.disabled,table td.settings_plugin_plugin_manager_plugins_actions a.disabled{color:#ccc;cursor:default}#settings_plugin_pluginmanager_repositorydialog .slimScrollDiv{margin-bottom:20px}#settings_plugin_pluginmanager_repositorydialog h4{position:relative}#settings_plugin_pluginmanager_repositorydialog h4 a.dropdown-toggle{color:inherit;text-decoration:none;font-size:14px}#settings_plugin_pluginmanager_repositorydialog h4 ul.dropdown-menu{font-size:14px}#settings_plugin_pluginmanager_repositorydialog .form-search{text-align:center;margin-bottom:5px!important}#settings_plugin_pluginmanager_repositorydialog .form-inline{padding:5px;padding-right:10px;margin-bottom:0}#settings_plugin_pluginmanager_repositorydialog .form-inline .help-block{margin-bottom:0;font-size:85%}#settings_plugin_pluginmanager_repositorydialog #settings_plugin_pluginmanager_repositorydialog_unavailable{overflow:hidden;width:100%;height:300px;background-image:url("../img/repo_unavailable.png");text-align:center;display:table}#settings_plugin_pluginmanager_repositorydialog #settings_plugin_pluginmanager_repositorydialog_unavailable div{display:table-cell;vertical-align:middle}#settings_plugin_pluginmanager_repositorydialog #settings_plugin_pluginmanager_repositorydialog_list{overflow:hidden;width:auto;height:300px}#settings_plugin_pluginmanager_repositorydialog #settings_plugin_pluginmanager_repositorydialog_list .entry{border-bottom:1px solid #ddd;padding:5px;padding-right:10px}#settings_plugin_pluginmanager_workingdialog_output .message{font-weight:bold}#settings_plugin_pluginmanager_workingdialog_output .stdout{color:#333}#settings_plugin_pluginmanager_workingdialog_output .stderr{color:#900}#settings_plugin_pluginmanager_workingdialog_output .call{color:#009} \ No newline at end of file +table td.settings_plugin_plugin_manager_plugins_name,table th.settings_plugin_plugin_manager_plugins_name{text-overflow:ellipsis;text-align:left}table td.settings_plugin_plugin_manager_plugins_actions,table th.settings_plugin_plugin_manager_plugins_actions{text-align:center;width:80px}table td.settings_plugin_plugin_manager_plugins_actions a,table th.settings_plugin_plugin_manager_plugins_actions a{text-decoration:none;color:#000}table td.settings_plugin_plugin_manager_plugins_actions a.disabled,table th.settings_plugin_plugin_manager_plugins_actions a.disabled{color:#ccc;cursor:default}#settings_plugin_pluginmanager_repositorydialog .slimScrollDiv{margin-bottom:20px}#settings_plugin_pluginmanager_repositorydialog h4{position:relative}#settings_plugin_pluginmanager_repositorydialog h4 a.dropdown-toggle{color:inherit;text-decoration:none;font-size:14px}#settings_plugin_pluginmanager_repositorydialog h4 ul.dropdown-menu{font-size:14px}#settings_plugin_pluginmanager_repositorydialog .form-search{text-align:center;margin-bottom:5px!important}#settings_plugin_pluginmanager_repositorydialog .form-inline{padding:5px 10px 5px 5px;margin-bottom:0}#settings_plugin_pluginmanager_repositorydialog .form-inline .help-block{margin-bottom:0;font-size:85%}#settings_plugin_pluginmanager_repositorydialog #settings_plugin_pluginmanager_repositorydialog_unavailable{overflow:hidden;width:100%;height:300px;background-image:url(../img/repo_unavailable.png);text-align:center;display:table}#settings_plugin_pluginmanager_repositorydialog #settings_plugin_pluginmanager_repositorydialog_unavailable div{display:table-cell;vertical-align:middle}#settings_plugin_pluginmanager_repositorydialog #settings_plugin_pluginmanager_repositorydialog_list{overflow:hidden;width:auto;height:300px}#settings_plugin_pluginmanager_repositorydialog #settings_plugin_pluginmanager_repositorydialog_list .entry{border-bottom:1px solid #ddd;padding:5px 10px 5px 5px}#settings_plugin_pluginmanager_workingdialog_output span{display:block}#settings_plugin_pluginmanager_workingdialog_output .message{font-weight:700}#settings_plugin_pluginmanager_workingdialog_output .error{font-weight:700;color:#900}#settings_plugin_pluginmanager_workingdialog_output .stdout{color:#333}#settings_plugin_pluginmanager_workingdialog_output .stderr{color:#900}#settings_plugin_pluginmanager_workingdialog_output .call{color:#009} \ No newline at end of file diff --git a/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js b/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js index 173f31cd18..0ee352e0e7 100644 --- a/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js +++ b/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js @@ -10,7 +10,20 @@ }; OctoPrintPluginManagerClient.prototype.get = function(refresh, opts) { - return this.base.get(this.base.getSimpleApiUrl("pluginmanager") + ((refresh) ? "?refresh_repository=true" : ""), opts); + var refresh_repo, refresh_notices; + if (_.isPlainObject(refresh)) { + refresh_repo = refresh.repo || false; + refresh_notices = refresh.notices || false; + } else { + refresh_repo = refresh; + refresh_notices = false; + } + + var query = []; + if (refresh_repo) query.push("refresh_repository=true"); + if (refresh_notices) query.push("refresh_notices=true"); + + return this.base.get(this.base.getSimpleApiUrl("pluginmanager") + ((query.length) ? "?" + query.join("&") : ""), opts); }; OctoPrintPluginManagerClient.prototype.getWithRefresh = function(opts) { @@ -79,6 +92,8 @@ $(function() { self.config_repositoryUrl = ko.observable(); self.config_repositoryTtl = ko.observable(); + self.config_noticesUrl = ko.observable(); + self.config_noticesTtl = ko.observable(); self.config_pipAdditionalArgs = ko.observable(); self.config_pipForceUser = ko.observable(); @@ -185,6 +200,20 @@ $(function() { }); self.notifications = []; + self.noticeNotifications = []; + self.hiddenNoticeNotifications = {}; + self.noticeCount = ko.observable(0); + + self.noticeCountText = ko.pureComputed(function() { + var count = self.noticeCount(); + if (count == 0) { + return gettext("There are no plugin notices. Great!"); + } else if (count == 1) { + return gettext("There is a plugin notice for one of your installed plugins."); + } else { + return _.sprintf(gettext("There are %(count)d plugin notices for one or more of your installed plugins."), {count: count}); + } + }); self.enableManagement = ko.pureComputed(function() { return !self.printerState.isPrinting(); @@ -250,7 +279,13 @@ $(function() { }); }, done: function(e, data) { - self._markDone(); + var response = data.result; + if (response.result) { + self._markDone(); + } else { + self._markDone(response.reason); + } + self.uploadButton.unbind("click"); self.uploadFilename(undefined); }, @@ -261,7 +296,7 @@ $(function() { type: "error", hide: false }); - self._markDone(); + self._markDone("Could not install plugin, unknown error."); self.uploadButton.unbind("click"); self.uploadFilename(undefined); } @@ -280,19 +315,36 @@ $(function() { return false; }; - self.fromResponse = function(data) { - self._fromPluginsResponse(data.plugins); - self._fromRepositoryResponse(data.repository); - self._fromPipResponse(data.pip); + self.fromResponse = function(data, options) { + self._fromPluginsResponse(data.plugins, options); + self._fromRepositoryResponse(data.repository, options); + self._fromPipResponse(data.pip, options); self.safeMode(data.safe_mode || false); }; - self._fromPluginsResponse = function(data) { + self._fromPluginsResponse = function(data, options) { + var evalNotices = options.eval_notices || false; + var ignoreNoticeHidden = options.ignore_notice_hidden || false; + var ignoreNoticeIgnored = options.ignore_notice_ignored || false; + + if (evalNotices) self._removeAllNoticeNotifications(); + var installedPlugins = []; + var noticeCount = 0; _.each(data, function(plugin) { installedPlugins.push(plugin.key); + + if (evalNotices && plugin.notifications && plugin.notifications.length) { + _.each(plugin.notifications, function(notification) { + noticeCount++; + if (!ignoreNoticeIgnored && self._isNoticeNotificationIgnored(plugin.key, notification.date)) return; + if (!ignoreNoticeHidden && self._isNoticeNotificationHidden(plugin.key, notification.date)) return; + self._showPluginNotification(plugin, notification); + }); + } }); + if (evalNotices) self.noticeCount(noticeCount); self.installedPlugins(installedPlugins); self.plugins.updateItems(data); }; @@ -324,13 +376,28 @@ $(function() { } }; - self.requestData = function(includeRepo) { + self.requestData = function(options) { if (!self.loginState.isAdmin()) { return; } - OctoPrint.plugins.pluginmanager.get(includeRepo) - .done(self.fromResponse); + if (!_.isPlainObject(options)) { + options = { + refresh_repo: options, + refresh_notices: false, + eval_notices: false + }; + + } + + options.refresh_repo = options.refresh_repo || false; + options.refresh_notices = options.refresh_notices || false; + options.eval_notices = options.eval_notices || false; + + OctoPrint.plugins.pluginmanager.get({repo: options.refresh_repo, notices: options.refresh_notices}) + .done(function(data) { + self.fromResponse(data, options); + }); }; self.togglePlugin = function(data) { @@ -344,7 +411,9 @@ $(function() { if (data.key == "pluginmanager") return; - var onSuccess = self.requestData, + var onSuccess = function() { + self.requestData(); + }, onError = function() { new PNotify({ title: gettext("Something went wrong"), @@ -360,9 +429,26 @@ $(function() { .done(onSuccess) .fail(onError); } else { - OctoPrint.plugins.pluginmanager.disable(data.key) - .done(onSuccess) - .fail(onError); + var perform = function() { + OctoPrint.plugins.pluginmanager.disable(data.key) + .done(onSuccess) + .fail(onError); + }; + + if (data.disabling_discouraged) { + var message = _.sprintf(gettext("You are about to disable \"%(name)s\"."), {name: data.name}) + + "

      " + data.disabling_discouraged; + showConfirmationDialog({ + title: gettext("This is not recommended"), + message: message, + question: gettext("Do you still want to disable it?"), + cancel: gettext("Keep enabled"), + proceed: gettext("Disable anyway"), + onproceed: perform + }) + } else { + perform(); + } } }; @@ -418,32 +504,33 @@ $(function() { } self._markWorking(workTitle, workText); - var onSuccess = function() { + var onSuccess = function(response) { + if (response.result) { + self._markDone(); + } else { + self._markDone(response.reason) + } self.requestData(); self.installUrl(""); }, onError = function() { + self._markDone("Could not install plugin, unknown error, please consult octoprint.log for details"); new PNotify({ title: gettext("Something went wrong"), text: gettext("Please consult octoprint.log for details"), type: "error", hide: false }); - }, - onAlways = function() { - self._markDone(); }; if (reinstall) { OctoPrint.plugins.pluginmanager.reinstall(reinstall, url, followDependencyLinks) .done(onSuccess) - .fail(onError) - .always(onAlways); + .fail(onError); } else { OctoPrint.plugins.pluginmanager.install(url, followDependencyLinks) .done(onSuccess) - .fail(onError) - .always(onAlways); + .fail(onError); } }; @@ -462,7 +549,9 @@ $(function() { self._markWorking(gettext("Uninstalling plugin..."), _.sprintf(gettext("Uninstalling plugin \"%(name)s\""), {name: data.name})); OctoPrint.plugins.pluginmanager.uninstall(data.key) - .done(self.requestData) + .done(function() { + self.requestData(); + }) .fail(function() { new PNotify({ title: gettext("Something went wrong"), @@ -480,8 +569,23 @@ $(function() { if (!self.loginState.isAdmin()) { return; } + self.requestData({refresh_repo: true}); + }; + + self.refreshNotices = function() { + if (!self.loginState.isAdmin()) { + return; + } - self.requestData(true); + self.requestData({refresh_notices: true, eval_notices: true, ignore_notice_hidden: true, ignore_notice_ignored: true}); + }; + + self.reshowNotices = function() { + if (!self.loginState.isAdmin()) { + return; + } + + self.requestData({eval_notices: true, ignore_notice_hidden: true, ignore_notice_ignored: true}); }; self.showPluginSettings = function() { @@ -502,6 +606,18 @@ $(function() { repositoryTtl = null; } + var notices = self.config_noticesUrl(); + if (notices != undefined && notices.trim() == "") { + notices = null; + } + + var noticesTtl; + try { + noticesTtl = parseInt(self.config_noticesTtl()); + } catch (ex) { + noticesTtl = null; + } + var pipArgs = self.config_pipAdditionalArgs(); if (pipArgs != undefined && pipArgs.trim() == "") { pipArgs = null; @@ -512,6 +628,8 @@ $(function() { pluginmanager: { repository: repository, repository_ttl: repositoryTtl, + notices: notices, + notices_ttl: noticesTtl, pip_args: pipArgs, pip_force_user: self.config_pipForceUser() } @@ -520,13 +638,15 @@ $(function() { self.settingsViewModel.saveData(data, function() { self.configurationDialog.modal("hide"); self._copyConfig(); - self.refreshRepository(); + self.requestData({refresh_repo: true, refresh_notices: true, eval_notices: true}); }); }; self._copyConfig = function() { self.config_repositoryUrl(self.settingsViewModel.settings.plugins.pluginmanager.repository()); self.config_repositoryTtl(self.settingsViewModel.settings.plugins.pluginmanager.repository_ttl()); + self.config_noticesUrl(self.settingsViewModel.settings.plugins.pluginmanager.notices()); + self.config_noticesTtl(self.settingsViewModel.settings.plugins.pluginmanager.notices_ttl()); self.config_pipAdditionalArgs(self.settingsViewModel.settings.plugins.pluginmanager.pip_args()); self.config_pipForceUser(self.settingsViewModel.settings.plugins.pluginmanager.pip_force_user()); }; @@ -540,7 +660,7 @@ $(function() { }; self.installButtonText = function(data) { - return self.isCompatible(data) ? (self.installed(data) ? gettext("Reinstall") : gettext("Install")) : gettext("Incompatible"); + return self.isCompatible(data) ? (self.installed(data) ? gettext("Reinstall") : gettext("Install")) : (data.disabled ? gettext("Disabled") : gettext("Incompatible")); }; self._displayNotification = function(response, titleSuccess, textSuccess, textRestart, textReload, titleError, textError) { @@ -654,9 +774,14 @@ $(function() { self.workingDialog.modal({keyboard: false, backdrop: "static", show: true}); }; - self._markDone = function() { + self._markDone = function(error) { self.working(false); - self.loglines.push({line: gettext("Done!"), stream: "message"}); + if (error) { + self.loglines.push({line: gettext("Error!"), stream: "error"}); + self.loglines.push({line: error, stream: "error"}) + } else { + self.loglines.push({line: gettext("Done!"), stream: "message"}); + } self._scrollWorkingOutputToEnd(); }; @@ -689,13 +814,184 @@ $(function() { } }; + self.showPluginNotifications = function(plugin) { + if (!plugin.notifications || plugin.notifications.length == 0) return; + + self._removeAllNoticeNotificationsForPlugin(plugin.key); + _.each(plugin.notifications, function(notification) { + self._showPluginNotification(plugin, notification); + }); + }; + + self.showPluginNotificationsLinkText = function(plugins) { + if (!plugins.notifications || plugins.notifications.length == 0) return; + + var count = plugins.notifications.length; + var importantCount = _.filter(plugins.notifications, function(notification) { return notification.important }).length; + if (count > 1) { + if (importantCount) { + return _.sprintf(gettext("There are %(count)d notices (%(important)d marked as important) available regarding this plugin - click to show!"), {count: count, important: importantCount}); + } else { + return _.sprintf(gettext("There are %(count)d notices available regarding this plugin - click to show!"), {count: count}); + } + } else { + if (importantCount) { + return gettext("There is an important notice available regarding this plugin - click to show!"); + } else { + return gettext("There is a notice available regarding this plugin - click to show!"); + } + } + }; + + self._showPluginNotification = function(plugin, notification) { + var name = plugin.name; + var version = plugin.version; + + var important = notification.important; + var link = notification.link; + + var title; + if (important) { + title = _.sprintf(gettext("Important notice regarding plugin \"%(name)s\""), {name: name}); + } else { + title = _.sprintf(gettext("Notice regarding plugin \"%(name)s\""), {name: name}); + } + + var text = ""; + + if (notification.versions && notification.versions.length > 0) { + var versions = _.map(notification.versions, function(v) { return (v == version) ? "" + v + "" : v; }).join(", "); + text += "" + _.sprintf(gettext("Affected versions: %(versions)s"), {versions: versions}) + ""; + } else { + text += "" + gettext("Affected versions: all") + ""; + } + + text += "

      " + notification.text + "

      "; + if (link) { + text += "

      " + gettext("Read more...") + "

      "; + } + + var beforeClose = function(notification) { + if (!self.noticeNotifications[plugin.key]) return; + self.noticeNotifications[plugin.key] = _.without(self.noticeNotifications[plugin.key], notification); + }; + + var options = { + title: title, + text: text, + type: (important) ? "error" : "notice", + before_close: beforeClose, + hide: false, + confirm: { + confirm: true, + buttons: [{ + text: gettext("Later"), + click: function(notice) { + self._hideNoticeNotification(plugin.key, notification.date); + notice.remove(); + notice.get().trigger("pnotify.cancel", notice); + } + }, { + text: gettext("Mark read"), + click: function(notice) { + self._ignoreNoticeNotification(plugin.key, notification.date); + notice.remove(); + notice.get().trigger("pnotify.cancel", notice); + } + }] + }, + buttons: { + sticker: false, + closer: false + } + }; + + if (!self.noticeNotifications[plugin.key]) { + self.noticeNotifications[plugin.key] = []; + } + self.noticeNotifications[plugin.key].push(new PNotify(options)); + }; + + self._removeAllNoticeNotifications = function() { + _.each(_.keys(self.noticeNotifications), function(key) { + self._removeAllNoticeNotificationsForPlugin(key); + }); + }; + + self._removeAllNoticeNotificationsForPlugin = function(key) { + if (!self.noticeNotifications[key] || !self.noticeNotifications[key].length) return; + _.each(self.noticeNotifications[key], function(notification) { + notification.remove(); + }); + }; + + self._hideNoticeNotification = function(key, date) { + if (!self.hiddenNoticeNotifications[key]) { + self.hiddenNoticeNotifications[key] = []; + } + if (!_.contains(self.hiddenNoticeNotifications[key], date)) { + self.hiddenNoticeNotifications[key].push(date); + } + }; + + self._isNoticeNotificationHidden = function(key, date) { + if (!self.hiddenNoticeNotifications[key]) return false; + return _.any(_.map(self.hiddenNoticeNotifications[key], function(d) { return date == d; })); + }; + + var noticeLocalStorageKey = "plugin.pluginmanager.seen_notices"; + self._ignoreNoticeNotification = function(key, date) { + if (!Modernizr.localstorage) + return false; + if (!self.loginState.isAdmin()) + return false; + + var currentString = localStorage[noticeLocalStorageKey]; + var current; + if (currentString === undefined) { + current = {}; + } else { + current = JSON.parse(currentString); + } + if (!current[self.loginState.username()]) { + current[self.loginState.username()] = {}; + } + if (!current[self.loginState.username()][key]) { + current[self.loginState.username()][key] = []; + } + + if (!_.contains(current[self.loginState.username()][key], date)) { + current[self.loginState.username()][key].push(date); + localStorage[noticeLocalStorageKey] = JSON.stringify(current); + } + }; + + self._isNoticeNotificationIgnored = function(key, date) { + if (!Modernizr.localstorage) + return false; + + if (localStorage[noticeLocalStorageKey] == undefined) + return false; + + var knownData = JSON.parse(localStorage[noticeLocalStorageKey]); + + if (!self.loginState.isAdmin()) + return true; + + var userData = knownData[self.loginState.username()]; + if (userData === undefined) + return false; + + return userData[key] && _.contains(userData[key], date); + }; + self.onBeforeBinding = function() { self.settings = self.settingsViewModel.settings; }; self.onUserLoggedIn = function(user) { if (user.admin) { - self.requestData(); + self.requestData({eval_notices: true}); } else { self.onUserLoggedOut(); } @@ -787,22 +1083,42 @@ $(function() { } titleError = gettext("Something went wrong"); - var url = "unknown"; - if (data.hasOwnProperty("url")) { - url = data.url; + var source = "unknown"; + if (data.hasOwnProperty("source")) { + source = data.source; + } + var sourceType = "unknown"; + if (data.hasOwnProperty("source_type")) { + sourceType = data.source_type; } if (data.hasOwnProperty("reason")) { if (data.was_reinstalled) { - textError = _.sprintf(gettext("Reinstalling the plugin from URL \"%(url)s\" failed: %(reason)s"), {reason: data.reason, url: url}); + if (sourceType == "path") { + textError = _.sprintf(gettext("Reinstalling the plugin from file failed: %(reason)s"), {reason: data.reason}); + } else { + textError = _.sprintf(gettext("Reinstalling the plugin from \"%(source)s\" failed: %(reason)s"), {reason: data.reason, source: source}); + } } else { - textError = _.sprintf(gettext("Installing the plugin from URL \"%(url)s\" failed: %(reason)s"), {reason: data.reason, url: url}); + if (sourceType == "path") { + textError = _.sprintf(gettext("Installing the plugin from file failed: %(reason)s"), {reason: data.reason}); + } else { + textError = _.sprintf(gettext("Installing the plugin from \"%(source)s\" failed: %(reason)s"), {reason: data.reason, source: source}); + } } } else { if (data.was_reinstalled) { - textError = _.sprintf(gettext("Reinstalling the plugin from URL \"%(url)s\" failed, please see the log for details."), {url: url}); + if (sourceType == "path") { + textError = gettext("Reinstalling the plugin from file failed, please see the log for details."); + } else { + textError = _.sprintf(gettext("Reinstalling the plugin from \"%(source)s\" failed, please see the log for details."), {source: source}); + } } else { - textError = _.sprintf(gettext("Installing the plugin from URL \"%(url)s\" failed, please see the log for details."), {url: url}); + if (sourceType == "path") { + textError = gettext("Installing the plugin from file failed, please see the log for details."); + } else { + textError = _.sprintf(gettext("Installing the plugin from \"%(source)s\" failed, please see the log for details."), {source: source}); + } } } diff --git a/src/octoprint/plugins/pluginmanager/static/less/pluginmanager.less b/src/octoprint/plugins/pluginmanager/static/less/pluginmanager.less index a14c2f6b3c..cd49e51db6 100644 --- a/src/octoprint/plugins/pluginmanager/static/less/pluginmanager.less +++ b/src/octoprint/plugins/pluginmanager/static/less/pluginmanager.less @@ -85,10 +85,19 @@ table { } #settings_plugin_pluginmanager_workingdialog_output { + span { + display: block; + } + .message { font-weight: bold; } + .error { + font-weight: bold; + color: #990000; + } + .stdout { color: #333333; } diff --git a/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 b/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 index 8f01183c0a..8e8e614f9c 100644 --- a/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 +++ b/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 @@ -40,7 +40,8 @@ -
      ()
      +
      ()
      +
       
      {{ _('Homepage') }} @@ -72,7 +73,7 @@
      - + Using pip of "", Version @@ -86,12 +87,28 @@
      + +
      @@ -237,6 +255,24 @@ + {{ _('Notices configuration') }} + +
      + +
      + +
      +
      +
      + +
      +
      + + min +
      +
      +
      + {{ _('pip configuration') }}
      diff --git a/src/octoprint/plugins/softwareupdate/__init__.py b/src/octoprint/plugins/softwareupdate/__init__.py index b3a0b7f3a8..507c5593ca 100644 --- a/src/octoprint/plugins/softwareupdate/__init__.py +++ b/src/octoprint/plugins/softwareupdate/__init__.py @@ -18,6 +18,7 @@ from . import version_checks, updaters, exceptions, util, cli +from flask.ext.babel import gettext from octoprint.server.util.flask import restricted_access, with_revalidation_checking, check_etag from octoprint.server import admin_permission, VERSION, REVISION, BRANCH @@ -34,6 +35,9 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, octoprint.plugin.TemplatePlugin, octoprint.plugin.StartupPlugin, octoprint.plugin.WizardPlugin): + + COMMIT_TRACKING_TYPES = ("github_commit", "bitbucket_commit") + def __init__(self): self._update_in_progress = False self._configured_checks_mutex = threading.Lock() @@ -46,6 +50,7 @@ def __init__(self): self._version_cache_ttl = 0 self._version_cache_path = None self._version_cache_dirty = False + self._version_cache_timestamp = None self._console_logger = None @@ -73,8 +78,14 @@ def on_startup(self, host, port): self._console_logger.propagate = False def on_after_startup(self): - # refresh cache now if necessary so it's faster once the user connects to the instance - self.get_current_versions() + # refresh cache now if necessary so it's faster once the user connects to the instance - but decouple it from + # the server startup + def fetch_data(): + self.get_current_versions() + + thread = threading.Thread(target=fetch_data) + thread.daemon = True + thread.start() def _get_configured_checks(self): with self._configured_checks_mutex: @@ -113,7 +124,7 @@ def _get_configured_checks(self): # This used to be part of the settings migration (version 2) due to a bug - it can't # stay there though since it interferes with manual entries to the checks not # originating from within a plugin. Hence we do that step now here. - if "type" not in effective_config or effective_config["type"] != "github_commit": + if "type" not in effective_config or effective_config["type"] not in self.COMMIT_TRACKING_TYPES: deletables = ["current", "displayVersion"] else: deletables = [] @@ -152,6 +163,7 @@ def _load_version_cache(self): try: with open(self._version_cache_path) as f: data = yaml.safe_load(f) + timestamp = os.stat(self._version_cache_path).st_mtime except: self._logger.exception("Error while loading version cache from disk") else: @@ -174,6 +186,7 @@ def _load_version_cache(self): self._version_cache = data self._version_cache_dirty = False + self._version_cache_timestamp = timestamp self._logger.info("Loaded version cache from disk") except: self._logger.exception("Error parsing in version cache data") @@ -190,6 +203,7 @@ def _save_version_cache(self): yaml.safe_dump(self._version_cache, stream=file_obj, default_flow_style=False, indent=" ", allow_unicode=True) self._version_cache_dirty = False + self._version_cache_timestamp = time.time() self._logger.info("Saved version cache to disk") #~~ SettingsPlugin API @@ -213,6 +227,8 @@ def get_settings_defaults(self): "check_providers": {}, "cache_ttl": 24 * 60, + + "notify_users": True } def on_settings_load(self): @@ -260,7 +276,7 @@ def on_settings_load(self): def on_settings_save(self, data): for key in self.get_settings_defaults(): - if key in ("checks", "cache_ttl", "octoprint_checkout_folder", "octoprint_type", "octoprint_release_channel"): + if key in ("checks", "cache_ttl", "notify_user", "octoprint_checkout_folder", "octoprint_type", "octoprint_release_channel"): continue if key in data: self._settings.set([key], data[key]) @@ -269,6 +285,9 @@ def on_settings_save(self, data): self._settings.set_int(["cache_ttl"], data["cache_ttl"]) self._version_cache_ttl = self._settings.get_int(["cache_ttl"]) * 60 + if "notify_users" in data: + self._settings.set_boolean(["notify_users"], data["notify_users"]) + checks = self._get_configured_checks() if "octoprint" in checks: check = checks["octoprint"] @@ -378,7 +397,7 @@ def on_settings_migrate(self, target, current=None): configured_checks = self._settings.get(["checks"], incl_defaults=False) if configured_checks is not None and "octoprint" in configured_checks: octoprint_check = dict(configured_checks["octoprint"]) - if "type" not in octoprint_check or octoprint_check["type"] != "github_commit": + if "type" not in octoprint_check or octoprint_check["type"] not in self.COMMIT_TRACKING_TYPES: deletables=["current", "displayName", "displayVersion"] else: deletables=[] @@ -454,7 +473,8 @@ def view(): data["check"]["python_updater"] = True return flask.jsonify(dict(status="updatePossible" if update_available and update_possible else "updateAvailable" if update_available else "current", - information=information)) + information=information, + timestamp=self._version_cache_timestamp)) except exceptions.ConfigurationInvalid as e: return flask.make_response("Update not properly configured, can't proceed: %s" % e.message, 500) @@ -480,6 +500,7 @@ def etag(): hash.update(str(data["possible"])) hash.update(",".join(targets)) + hash.update(str(self._version_cache_timestamp)) return hash.hexdigest() def condition(): @@ -825,7 +846,7 @@ def _perform_update(self, target, check, force): self._settings.load() # persist the new version if necessary for check type - if check["type"] == "github_commit": + if check["type"] in self.COMMIT_TRACKING_TYPES: dummy_default = dict(plugins=dict()) dummy_default["plugins"][self._identifier] = dict(checks=dict()) dummy_default["plugins"][self._identifier]["checks"][target] = dict(current=None) @@ -882,7 +903,7 @@ def _populated_check(self, target, check): release_branches += [x["branch"] for x in check["prerelease_branches"]] result["released_version"] = not release_branches or BRANCH in release_branches - if check["type"] == "github_commit": + if check["type"] in self.COMMIT_TRACKING_TYPES: result["current"] = REVISION if REVISION else "unknown" else: result["current"] = VERSION @@ -936,7 +957,7 @@ def _populated_check(self, target, check): # displayVersion AND current missing or None result["displayVersion"] = u"unknown" - if check["type"] in ("github_commit",): + if check["type"] in self.COMMIT_TRACKING_TYPES: result["current"] = check.get("current", None) else: result["current"] = check.get("current", check.get("displayVersion", None)) @@ -968,18 +989,11 @@ def _get_version_checker(self, target, check): raise exceptions.ConfigurationInvalid("no check type defined") check_type = check["type"] - if check_type == "github_release": - return version_checks.github_release - elif check_type == "github_commit": - return version_checks.github_commit - elif check_type == "git_commit": - return version_checks.git_commit - elif check_type == "commandline": - return version_checks.commandline - elif check_type == "python_checker": - return version_checks.python_checker - else: + method = getattr(version_checks, check_type) + if method is None: raise exceptions.UnknownCheckType() + else: + return method def _get_update_method(self, target, check, valid_methods=None): """ @@ -1038,8 +1052,11 @@ def _get_octoprint_checkout_folder(self, checks=None): __plugin_name__ = "Software Update" __plugin_author__ = "Gina Häußge" -__plugin_url__ = "https://github.com/foosel/OctoPrint/wiki/Plugin:-Software-Update" +__plugin_url__ = "http://docs.octoprint.org/en/master/bundledplugins/softwareupdate.html" __plugin_description__ = "Allows receiving update notifications and performing updates of OctoPrint and plugins" +__plugin_disabling_discouraged__ = gettext("Without this plugin OctoPrint will no longer be able to " + "update itself or any of your installed plugins which might put " + "your system at risk.") __plugin_license__ = "AGPLv3" def __plugin_load__(): global __plugin_implementation__ diff --git a/src/octoprint/plugins/softwareupdate/static/css/softwareupdate.css b/src/octoprint/plugins/softwareupdate/static/css/softwareupdate.css index cace183b37..7aa306f96c 100644 --- a/src/octoprint/plugins/softwareupdate/static/css/softwareupdate.css +++ b/src/octoprint/plugins/softwareupdate/static/css/softwareupdate.css @@ -1 +1 @@ -td.settings_plugin_softwareupdate_column_update{width:16px}#settings_plugin_softwareupdate_workingdialog_output{font-size:.8em}#settings_plugin_softwareupdate_workingdialog_output .message{font-weight:bold}#settings_plugin_softwareupdate_workingdialog_output .separator{font-weight:bold;color:#666}#settings_plugin_softwareupdate_workingdialog_output .stdout{color:#333}#settings_plugin_softwareupdate_workingdialog_output .stderr{color:#900}#settings_plugin_softwareupdate_workingdialog_output .call{color:#009}#settings_plugin_softwareupdate_workingdialog_output .message_error{font-weight:bold;color:#900}.softwareupdate_notification ul{margin:10px 0 10px 25px}.softwareupdate_notification ul .name{text-overflow:ellipsis;white-space:nowrap;overflow:hidden;display:block} \ No newline at end of file +td.settings_plugin_softwareupdate_column_update{width:16px}#settings_plugin_softwareupdate_workingdialog_output{font-size:.8em}#settings_plugin_softwareupdate_workingdialog_output span{display:block}#settings_plugin_softwareupdate_workingdialog_output .message{font-weight:700}#settings_plugin_softwareupdate_workingdialog_output .separator{font-weight:700;color:#666}#settings_plugin_softwareupdate_workingdialog_output .stdout{color:#333}#settings_plugin_softwareupdate_workingdialog_output .stderr{color:#900}#settings_plugin_softwareupdate_workingdialog_output .call{color:#009}#settings_plugin_softwareupdate_workingdialog_output .message_error{font-weight:700;color:#900}.softwareupdate_notification ul{margin:10px 0 10px 25px}.softwareupdate_notification ul .name{text-overflow:ellipsis;white-space:nowrap;overflow:hidden;display:block} \ No newline at end of file diff --git a/src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js b/src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js index 019b424055..cfc20cb5d1 100644 --- a/src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js +++ b/src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js @@ -111,7 +111,13 @@ $(function() { self.octoprintUnconfigured = ko.observable(); self.octoprintUnreleased = ko.observable(); + self.cacheTimestamp = ko.observable(); + self.cacheTimestampText = ko.pureComputed(function() { + return formatDate(self.cacheTimestamp()); + }); + self.config_cacheTtl = ko.observable(); + self.config_notifyUsers = ko.observable(); self.config_checkoutFolder = ko.observable(); self.config_checkType = ko.observable(); self.config_updateMethod = ko.observable(); @@ -153,9 +159,20 @@ $(function() { self.performCheck(); }; - self._showPopup = function(options, eventListeners) { + self.onUserLoggedOut = function() { + self._closePopup(); + }; + + self._showPopup = function(options, eventListeners, singleButtonNotify) { + singleButtonNotify = singleButtonNotify || false; + self._closePopup(); - self.popup = new PNotify(options); + + if (singleButtonNotify) { + self.popup = PNotify.singleButtonNotify(options); + } else { + self.popup = new PNotify(options); + } if (eventListeners) { var popupObj = self.popup.get(); @@ -192,6 +209,7 @@ $(function() { plugins: { softwareupdate: { cache_ttl: parseInt(self.config_cacheTtl()), + notify_users: self.config_notifyUsers(), octoprint_checkout_folder: self.config_checkoutFolder(), octoprint_type: self.config_checkType(), octoprint_release_channel: self.config_releaseChannel() @@ -231,6 +249,7 @@ $(function() { self.config_updateMethod(updateMethod); self.config_cacheTtl(self.settings.settings.plugins.softwareupdate.cache_ttl()); + self.config_notifyUsers(self.settings.settings.plugins.softwareupdate.notify_users()); self.config_checkoutFolder(self.settings.settings.plugins.softwareupdate.octoprint_checkout_folder()); self.config_checkType(self.settings.settings.plugins.softwareupdate.octoprint_type()); self.config_releaseChannel(self.settings.settings.plugins.softwareupdate.octoprint_release_channel()); @@ -242,6 +261,8 @@ $(function() { }; self.fromCheckResponse = function(data, ignoreSeen, showIfNothingNew) { + self.cacheTimestamp(data.timestamp); + var versions = []; _.each(data.information, function(value, key) { value["key"] = key; @@ -288,6 +309,8 @@ $(function() { } } + if (!self.loginState.isAdmin() && !self.settings.settings.plugins.softwareupdate.notify_users()) return; + if (data.status == "updateAvailable" || data.status == "updatePossible") { var text = "
      " + gettext("There are updates available for the following components:"); @@ -303,7 +326,11 @@ $(function() { }); text += "
    "; - text += "" + gettext("Those components marked with can be updated directly.") + ""; + text += "

    " + gettext("Those components marked with can be updated directly.") + "

    "; + + if (!self.loginState.isAdmin()) { + text += "

    " + gettext("To have updates applied, get in touch with an administrator of this OctoPrint instance.") + "

    "; + } text += ""; @@ -314,8 +341,9 @@ $(function() { }; var eventListeners = {}; + var singleButtonNotify = false; if (data.status == "updatePossible" && self.loginState.isAdmin()) { - // if user is admin, add action buttons + // if update is possible and user is admin, add action buttons for ignore and update options["confirm"] = { confirm: true, buttons: [{ @@ -336,10 +364,27 @@ $(function() { closer: false, sticker: false }; + } else { + // if update is not possible or user is not admin, only add ignore button + options["confirm"] = { + confirm: true, + buttons: [{ + text: gettext("Ignore"), + click: function(notice) { + notice.remove(); + self._markNotificationAsSeen(data.information); + } + }] + }; + options["buttons"] = { + closer: false, + sticker: false + }; + singleButtonNotify = true; } if ((ignoreSeen || !self._hasNotificationBeenSeen(data.information)) && !OctoPrint.coreui.wizardOpen) { - self._showPopup(options, eventListeners); + self._showPopup(options, eventListeners, singleButtonNotify); } } else if (data.status == "current") { if (showIfNothingNew) { @@ -355,7 +400,8 @@ $(function() { }; self.performCheck = function(showIfNothingNew, force, ignoreSeen) { - if (!self.loginState.isUser()) return; + if (!self.loginState.isAdmin() && !self.settings.settings.plugins.softwareupdate.notify_users()) return; + self.checking(true); OctoPrint.plugins.softwareupdate.check(force) .done(function(data) { @@ -369,7 +415,18 @@ $(function() { self._markNotificationAsSeen = function(data) { if (!Modernizr.localstorage) return false; - localStorage["plugin.softwareupdate.seen_information"] = JSON.stringify(self._informationToRemoteVersions(data)); + if (!self.loginState.isUser()) + return false; + + var currentString = localStorage["plugin.softwareupdate.seen_information"]; + var current; + if (currentString === undefined) { + current = {}; + } else { + current = JSON.parse(currentString); + } + current[self.loginState.username()] = self._informationToRemoteVersions(data); + localStorage["plugin.softwareupdate.seen_information"] = JSON.stringify(current); }; self._hasNotificationBeenSeen = function(data) { @@ -380,11 +437,19 @@ $(function() { return false; var knownData = JSON.parse(localStorage["plugin.softwareupdate.seen_information"]); + + if (!self.loginState.isUser()) + return true; + + var userData = knownData[self.loginState.username()]; + if (userData === undefined) + return false; + var freshData = self._informationToRemoteVersions(data); var hasBeenSeen = true; _.each(freshData, function(value, key) { - if (!_.has(knownData, key) || knownData[key] != freshData[key]) { + if (!_.has(userData, key) || userData[key] != freshData[key]) { hasBeenSeen = false; } }); @@ -400,6 +465,8 @@ $(function() { }; self.performUpdate = function(force, items) { + if (!self.loginState.isAdmin()) return; + self.updateInProgress = true; var options = { diff --git a/src/octoprint/plugins/softwareupdate/static/less/softwareupdate.less b/src/octoprint/plugins/softwareupdate/static/less/softwareupdate.less index 0065ed9581..aeb84d1e8f 100644 --- a/src/octoprint/plugins/softwareupdate/static/less/softwareupdate.less +++ b/src/octoprint/plugins/softwareupdate/static/less/softwareupdate.less @@ -5,6 +5,10 @@ td.settings_plugin_softwareupdate_column_update { #settings_plugin_softwareupdate_workingdialog_output { font-size: 0.8em; + span { + display: block; + } + .message { font-weight: bold; } diff --git a/src/octoprint/plugins/softwareupdate/templates/softwareupdate_settings.jinja2 b/src/octoprint/plugins/softwareupdate/templates/softwareupdate_settings.jinja2 index 27adb7e6a0..1120d04e64 100644 --- a/src/octoprint/plugins/softwareupdate/templates/softwareupdate_settings.jinja2 +++ b/src/octoprint/plugins/softwareupdate/templates/softwareupdate_settings.jinja2 @@ -62,8 +62,9 @@
    + {{ _('Last cache refresh:') }} - +
    @@ -86,6 +87,14 @@ +
    +
    + +
    +
    diff --git a/src/octoprint/static/js/app/client/settings.js b/src/octoprint/static/js/app/client/settings.js index 81157bc070..b1bd6e6277 100644 --- a/src/octoprint/static/js/app/client/settings.js +++ b/src/octoprint/static/js/app/client/settings.js @@ -6,6 +6,7 @@ } })(this, function(OctoPrintClient, $) { var url = "api/settings"; + var apiKeyUrl = url + "/apikey"; var OctoPrintSettingsClient = function(base) { this.base = base; @@ -40,6 +41,10 @@ return this.save(data, opts); }; + OctoPrintSettingsClient.prototype.generateApiKey = function (opts) { + return this.base.postJson(apiKeyUrl, opts); + }; + OctoPrintClient.registerComponent("settings", OctoPrintSettingsClient); return OctoPrintSettingsClient; }); diff --git a/src/octoprint/static/js/app/client/socket.js b/src/octoprint/static/js/app/client/socket.js index 32626a52a5..c12030a11a 100644 --- a/src/octoprint/static/js/app/client/socket.js +++ b/src/octoprint/static/js/app/client/socket.js @@ -27,10 +27,6 @@ }; OctoPrintSocketClient.prototype.propagateMessage = function(event, data) { - if (!this.registeredHandlers.hasOwnProperty(event)) { - return; - } - var start = new Date().getTime(); var eventObj = {event: event, data: data}; diff --git a/src/octoprint/static/js/app/helpers.js b/src/octoprint/static/js/app/helpers.js index 8badcc3e89..98a24d5b7e 100644 --- a/src/octoprint/static/js/app/helpers.js +++ b/src/octoprint/static/js/app/helpers.js @@ -162,6 +162,12 @@ function ItemListHelper(listType, supportedSorting, supportedFilters, defaultSor return undefined; }; + self.resetPage = function() { + if (self.currentPage() > self.lastPage()) { + self.currentPage(self.lastPage()); + } + }; + //~~ searching self.changeSearchFunction = function(searchFunction) { @@ -392,7 +398,7 @@ function formatFuzzyPrintTime(totalSeconds) { * * Accuracy decreases the higher the estimation is: * - * * less than 30s: "a couple of seconds" + * * less than 30s: "a few seconds" * * 30s to a minute: "less than a minute" * * 1 to 30min: rounded to full minutes, above 30s is minute + 1 ("27 minutes", "2 minutes") * * 30min to 40min: "40 minutes" @@ -489,7 +495,7 @@ function formatFuzzyPrintTime(totalSeconds) { } else { // only seconds if (seconds < 30) { - text = gettext("a couple of seconds"); + text = gettext("a few seconds"); } else { text = gettext("less than a minute"); } @@ -670,10 +676,163 @@ function showConfirmationDialog(msg, onacknowledge, options) { return modal; } +/** + * Shows a progress modal depending on a supplied promise. + * + * Will listen to the supplied promise, update the progress on .progress events and + * enabling the close button and (optionally) closing the dialog on promise resolve. + * + * The calling code should call "notify" on the deferred backing the promise and supply + * two parameters: the text to display on the progress bar and the optional output field and + * a boolean value indicating whether the operation behind that update was successful or not. + * Non-successful progress updates will remove the barClassSuccess class from the progress bar and + * apply the barClassFailure class and also apply the outputClassFailure to the produced line + * in the output. + * + * To determine the progress, calling code should supply the prognosed maximum number of + * progress events. An internal counter will increment on each progress event and used together + * with the max value to calculate the percentage to display on the progress bar. + * + * If no max value is set, the progress bar will show a striped animation at 100% fill status + * to visualize "unknown but ongoing" status. + * + * Available options: + * + * * title: the title of the modal, defaults to "Progress" + * * message: the message of the modal, defaults to "" + * * buttonText: the text on the close button, defaults to "Close" + * * max: maximum number of expected progress events (when 100% will be reached), defaults + * to undefined + * * close: whether to close the dialog on completion, defaults to false + * * output: whether to display the progress texts in an output field, defaults to false + * * dialogClass: additional class to apply to the dialog div + * * barClassSuccess: additional class for the progress bar while all progress events are + * successful + * * barClassFailure: additional class for the progress bar when a progress event was + * unsuccessful + * * outputClassSuccess: additional class for successful output lines + * * outputClassFailure: additional class for unsuccessful output lines + * + * @param options modal options + * @param promise promise to monitor + * @returns {*|jQuery} the modal object + */ +function showProgressModal(options, promise) { + var title = options.title || gettext("Progress"); + var message = options.message || ""; + var buttonText = options.button || gettext("Close"); + var max = options.max || undefined; + var close = options.close || false; + var output = options.output || false; + + var dialogClass = options.dialogClass || ""; + var barClassSuccess = options.barClassSuccess || ""; + var barClassFailure = options.barClassFailure || "bar-danger"; + var outputClassSuccess = options.outputClassSuccess || ""; + var outputClassFailure = options.outputClassFailure || "text-error"; + + var modalHeader = $('

    ' + title + '

    '); + var paragraph = $('

    ' + message + '

    '); + + var progress = $('
    '); + var progressBar = $('
    ') + .addClass(barClassSuccess); + var progressTextBack = $(''); + var progressTextFront = $('') + .width(progress.width()); + + if (max == undefined) { + progress.addClass("progress-striped active"); + progressBar.width("100%"); + } + + progressBar + .append(progressTextFront); + progress + .append(progressTextBack) + .append(progressBar); + + var button = $('') + .prop("disabled", true) + .attr("data-dismiss", "modal") + .attr("aria-hidden", "true"); + + var modalBody = $('
    ') + .addClass('modal-body') + .append(paragraph) + .append(progress); + + var pre; + if (output) { + pre = $("
    ");
    +        modalBody.append(pre);
    +    }
    +
    +    var modal = $('
    ') + .addClass('modal hide fade') + .addClass(dialogClass) + .append($('
    ').addClass('modal-header').append(modalHeader)) + .append(modalBody) + .append($('
    ').addClass('modal-footer').append(button)); + modal.modal({keyboard: false, backdrop: "static", show: true}); + + var counter = 0; + promise + .progress(function(text, success) { + var value; + + if (max === undefined || max <= 0) { + value = 100; + } else { + counter++; + value = Math.max(Math.min(counter * 100 / max, 100), 0); + } + + // update progress bar + progressBar.width(String(value) + "%"); + progressTextFront.text(text); + progressTextBack.text(text); + progressTextFront.width(progress.width()); + + // if not successful, apply failure class + if (!success && !progressBar.hasClass(barClassFailure)) { + progressBar + .removeClass(barClassSuccess) + .addClass(barClassFailure); + } + + if (output && pre) { + if (success) { + pre.append($("" + text + "
    ")); + } else { + pre.append($("" + text + "
    ")); + } + pre.scrollTop(pre[0].scrollHeight - pre.height()); + } + }) + .done(function() { + button.prop("disabled", false); + if (close) { + modal.modal("hide"); + } + }) + .fail(function() { + button.prop("disabled", false); + }); + + return modal; +} + function showReloadOverlay() { $("#reloadui_overlay").show(); } +function wrapPromiseWithAlways(p) { + var deferred = $.Deferred(); + p.always(function() { deferred.resolve.apply(deferred, arguments); }); + return deferred.promise(); +} + function commentableLinesToArray(lines) { return splitTextToArray(lines, "\n", true, function(item) {return !_.startsWith(item, "#")}); } diff --git a/src/octoprint/static/js/app/main.js b/src/octoprint/static/js/app/main.js index 6ccb0f9f50..3e2f6c704a 100644 --- a/src/octoprint/static/js/app/main.js +++ b/src/octoprint/static/js/app/main.js @@ -444,6 +444,18 @@ $(function() { } }; + $.fn.lazyload = function() { + return this.each(function() { + if (this.tagName.toLowerCase() != "img") return; + + var src = this.getAttribute("data-src"); + if (src) { + this.setAttribute("src", src); + this.removeAttribute("data-src"); + } + }); + }; + // Use bootstrap tabdrop for tabs and pills $('.nav-pills, .nav-tabs').tabdrop(); diff --git a/src/octoprint/static/js/app/viewmodels/control.js b/src/octoprint/static/js/app/viewmodels/control.js index d878eef560..544eeeede7 100644 --- a/src/octoprint/static/js/app/viewmodels/control.js +++ b/src/octoprint/static/js/app/viewmodels/control.js @@ -37,6 +37,8 @@ $(function() { self.additionalControls = []; self.webcamDisableTimeout = undefined; + self.webcamLoaded = ko.observable(false); + self.webcamError = ko.observable(false); self.keycontrolActive = ko.observable(false); self.keycontrolHelpActive = ko.observable(false); @@ -47,6 +49,14 @@ $(function() { return self.keycontrolActive() && self.keycontrolPossible(); }); + self.webcamRatioClass = ko.pureComputed(function() { + if (self.settings.webcam_streamRatio() == "4:3") { + return "ratio43"; + } else { + return "ratio169"; + } + }); + self.settings.printerProfiles.currentProfileData.subscribe(function () { self._updateExtruderCount(); self.settings.printerProfiles.currentProfileData().extruder.count.subscribe(self._updateExtruderCount); @@ -343,42 +353,24 @@ $(function() { self.requestData(); }; - self.updateRotatorWidth = function() { - var webcamImage = $("#webcam_image"); - if (self.settings.webcam_rotate90()) { - if (webcamImage.width() > 0) { - $("#webcam_rotator").css("height", webcamImage.width()); - } else { - webcamImage.off("load.rotator"); - webcamImage.on("load.rotator", function() { - $("#webcam_rotator").css("height", webcamImage.width()); - webcamImage.off("load.rotator"); - }); - } - } else { - $("#webcam_rotator").css("height", ""); - } - }; - - self.onSettingsBeforeSave = self.updateRotatorWidth; - self._isSafari = function() { var is_chrome = navigator.userAgent.indexOf('Chrome') > -1; var is_safari = navigator.userAgent.indexOf("Safari") > -1; return is_safari && !is_chrome; - } + }; self._disableWebcam = function() { // only disable webcam stream if tab is out of focus for more than 5s, otherwise we might cause // more load by the constant connection creation than by the actual webcam stream - + // safari bug doesn't release the mjpeg stream, so we just disable this for safari. if (self._isSafari()) { return; } - + self.webcamDisableTimeout = setTimeout(function () { $("#webcam_image").attr("src", ""); + self.webcamLoaded(false); }, 5000); }; @@ -392,12 +384,12 @@ $(function() { } var webcamImage = $("#webcam_image"); var currentSrc = webcamImage.attr("src"); - + // safari bug doesn't release the mjpeg stream, so we just set it up the once if (self._isSafari() && currentSrc != undefined) { return; } - + var newSrc = self.settings.webcam_streamUrl(); if (currentSrc != newSrc) { if (newSrc.lastIndexOf("?") > -1) { @@ -407,11 +399,24 @@ $(function() { } newSrc += new Date().getTime(); - self.updateRotatorWidth(); + self.webcamLoaded(false); + self.webcamError(false); webcamImage.attr("src", newSrc); } }; + self.onWebcamLoaded = function() { + log.debug("Webcam stream loaded"); + self.webcamLoaded(true); + self.webcamError(false); + }; + + self.onWebcamErrored = function() { + log.debug("Webcam stream failed to load/disabled"); + self.webcamLoaded(false); + self.webcamError(true); + }; + self.onTabChange = function (current, previous) { if (current == "#control") { self._enableWebcam(); diff --git a/src/octoprint/static/js/app/viewmodels/files.js b/src/octoprint/static/js/app/viewmodels/files.js index 75bb009261..5da2cd3a58 100644 --- a/src/octoprint/static/js/app/viewmodels/files.js +++ b/src/octoprint/static/js/app/viewmodels/files.js @@ -59,6 +59,14 @@ $(function() { self.localTarget = undefined; self.sdTarget = undefined; + self.dropOverlay = undefined; + self.dropZone = undefined; + self.dropZoneLocal = undefined; + self.dropZoneSd = undefined; + self.dropZoneBackground = undefined; + self.dropZoneLocalBackground = undefined; + self.dropZoneSdBackground = undefined; + self.ignoreUpdatedFilesEvent = false; self.addingFolder = ko.observable(false); @@ -395,14 +403,21 @@ $(function() { } }; - self.loadFile = function(file, printAfterLoad) { - if (!file) { + self.loadFile = function(data, printAfterLoad) { + if (!data) { return; } - var withinPrintDimensions = self.evaluatePrintDimensions(file, true); - var print = printAfterLoad && withinPrintDimensions; - OctoPrint.files.select(file.origin, file.path, print); + if (printAfterLoad && self.listHelper.isSelected(data) && self.enablePrint(data)) { + // file was already selected, just start the print job + OctoPrint.job.start(); + } else { + // select file, start print job (if requested and within dimensions) + var withinPrintDimensions = self.evaluatePrintDimensions(data, true); + var print = printAfterLoad && withinPrintDimensions; + + OctoPrint.files.select(data.origin, data.path, print); + } }; self.removeFile = function(file, event) { @@ -545,8 +560,11 @@ $(function() { }; self.enableSelect = function(data, printAfterSelect) { - var isLoadActionPossible = self.loginState.isUser() && self.isOperational() && !(self.isPrinting() || self.isPaused() || self.isLoading()); - return isLoadActionPossible && !self.listHelper.isSelected(data); + return self.enablePrint(data) && !self.listHelper.isSelected(data); + }; + + self.enablePrint = function(data) { + return self.loginState.isUser() && self.isOperational() && !(self.isPrinting() || self.isPaused() || self.isLoading()); }; self.enableSlicing = function(data) { @@ -811,6 +829,16 @@ $(function() { } self.sdTarget = $("#drop_sd"); + self.dropOverlay = $("#drop_overlay"); + self.dropZone = $("#drop"); + self.dropZoneLocal = $("#drop_locally"); + self.dropZoneSd = $("#drop_sd"); + self.dropZoneBackground = $("#drop_background"); + self.dropZoneLocalBackground = $("#drop_locally_background"); + self.dropZoneSdBackground = $("#drop_sd_background"); + + self.dropOverlay.on('drop', self._forceEndDragNDrop); + function evaluateDropzones() { var enableLocal = self.loginState.isUser(); var enableSd = enableLocal && CONFIG_SD_SUPPORT && self.printerState.isSdReady(); @@ -962,10 +990,12 @@ $(function() { self._enableDragNDrop = function(enable) { if (enable) { - $(document).bind("dragover", self._handleDragNDrop); + $(document).bind("dragenter", self._handleDragNDrop); + $(document).bind("dragleave", self._endDragNDrop); log.debug("Enabled drag-n-drop"); } else { - $(document).unbind("dragover", self._handleDragNDrop); + $(document).unbind("dragenter", self._handleDragNDrop); + $(document).unbind("dragleave", self._endDragNDrop); log.debug("Disabled drag-n-drop"); } }; @@ -1038,34 +1068,35 @@ $(function() { self._setProgressBar(progress, uploaded ? gettext("Saving ...") : gettext("Uploading ..."), uploaded); }; + self._dragNDropTarget = null; + self._forceEndDragNDrop = function () { + self.dropOverlay.removeClass("in"); + if (self.dropZoneLocal) self.dropZoneLocalBackground.removeClass("hover"); + if (self.dropZoneSd) self.dropZoneSdBackground.removeClass("hover"); + if (self.dropZone) self.dropZoneBackground.removeClass("hover"); + self._dragNDropTarget = null; + }; + + self._endDragNDrop = function (e) { + if (e.target != self._dragNDropTarget) return; + self._forceEndDragNDrop(); + }; + self._handleDragNDrop = function (e) { - var dropOverlay = $("#drop_overlay"); - var dropZone = $("#drop"); - var dropZoneLocal = $("#drop_locally"); - var dropZoneSd = $("#drop_sd"); - var dropZoneBackground = $("#drop_background"); - var dropZoneLocalBackground = $("#drop_locally_background"); - var dropZoneSdBackground = $("#drop_sd_background"); - var timeout = window.dropZoneTimeout; - - if (!timeout) { - dropOverlay.addClass('in'); - } else { - clearTimeout(timeout); - } + self.dropOverlay.addClass('in'); var foundLocal = false; var foundSd = false; var found = false; var node = e.target; do { - if (dropZoneLocal && node === dropZoneLocal[0]) { + if (self.dropZoneLocal && node === self.dropZoneLocal[0]) { foundLocal = true; break; - } else if (dropZoneSd && node === dropZoneSd[0]) { + } else if (self.dropZoneSd && node === self.dropZoneSd[0]) { foundSd = true; break; - } else if (dropZone && node === dropZone[0]) { + } else if (self.dropZone && node === self.dropZone[0]) { found = true; break; } @@ -1073,26 +1104,19 @@ $(function() { } while (node != null); if (foundLocal) { - dropZoneLocalBackground.addClass("hover"); - dropZoneSdBackground.removeClass("hover"); + self.dropZoneLocalBackground.addClass("hover"); + self.dropZoneSdBackground.removeClass("hover"); } else if (foundSd && self.printerState.isSdReady()) { - dropZoneSdBackground.addClass("hover"); - dropZoneLocalBackground.removeClass("hover"); + self.dropZoneSdBackground.addClass("hover"); + self.dropZoneLocalBackground.removeClass("hover"); } else if (found) { - dropZoneBackground.addClass("hover"); + self.dropZoneBackground.addClass("hover"); } else { - if (dropZoneLocalBackground) dropZoneLocalBackground.removeClass("hover"); - if (dropZoneSdBackground) dropZoneSdBackground.removeClass("hover"); - if (dropZoneBackground) dropZoneBackground.removeClass("hover"); + if (self.dropZoneLocalBackground) self.dropZoneLocalBackground.removeClass("hover"); + if (self.dropZoneSdBackground) self.dropZoneSdBackground.removeClass("hover"); + if (self.dropZoneBackground) self.dropZoneBackground.removeClass("hover"); } - - window.dropZoneTimeout = setTimeout(function () { - window.dropZoneTimeout = null; - dropOverlay.removeClass("in"); - if (dropZoneLocal) dropZoneLocalBackground.removeClass("hover"); - if (dropZoneSd) dropZoneSdBackground.removeClass("hover"); - if (dropZone) dropZoneBackground.removeClass("hover"); - }, 100); + self._dragNDropTarget = e.target; } } diff --git a/src/octoprint/static/js/app/viewmodels/gcode.js b/src/octoprint/static/js/app/viewmodels/gcode.js index ea8b4c6ea7..f1611873ae 100644 --- a/src/octoprint/static/js/app/viewmodels/gcode.js +++ b/src/octoprint/static/js/app/viewmodels/gcode.js @@ -138,6 +138,14 @@ $(function() { } }); + self.settings.feature_g90InfluencesExtruder.subscribe(function() { + GCODE.ui.updateOptions({ + reader: { + g90InfluencesExtruder: self.settings.feature_g90InfluencesExtruder() + } + }); + }); + self._retrieveBedDimensions = function(currentProfileData) { if (currentProfileData == undefined) { currentProfileData = self.settings.printerProfiles.currentProfileData(); diff --git a/src/octoprint/static/js/app/viewmodels/printerprofiles.js b/src/octoprint/static/js/app/viewmodels/printerprofiles.js index b4d76e22cc..f19484a640 100644 --- a/src/octoprint/static/js/app/viewmodels/printerprofiles.js +++ b/src/octoprint/static/js/app/viewmodels/printerprofiles.js @@ -444,8 +444,16 @@ $(function() { self.updateProfile(profile); }; + self.canMakeDefault = function(data) { + return !data.isdefault(); + }; + + self.canRemove = function(data) { + return !data.iscurrent() && !data.isdefault(); + }; + self.requestData = function() { - OctoPrint.printerprofiles.list() + return OctoPrint.printerprofiles.list() .done(self.fromResponse); }; @@ -482,9 +490,9 @@ $(function() { } self.requestData(); }) - .fail(function() { + .fail(function(xhr) { var text = gettext("There was unexpected error while saving the printer profile, please consult the logs."); - new PNotify({title: gettext("Saving failed"), text: text, type: "error", hide: false}); + new PNotify({title: gettext("Could not add profile"), text: text, type: "error", hide: false}); }) .always(function() { self.requestInProgress(false); @@ -492,18 +500,29 @@ $(function() { }; self.removeProfile = function(data) { - self.requestInProgress(true); - OctoPrint.printerprofiles.delete(data.id, {url: data.resource}) - .done(function() { - self.requestData(); - }) - .fail(function() { - var text = gettext("There was unexpected error while removing the printer profile, please consult the logs."); - new PNotify({title: gettext("Saving failed"), text: text, type: "error", hide: false}); - }) - .always(function() { - self.requestInProgress(false); - }); + var perform = function() { + self.requestInProgress(true); + OctoPrint.printerprofiles.delete(data.id, {url: data.resource}) + .done(function() { + self.requestData() + .always(function() { + self.requestInProgress(false); + }); + }) + .fail(function(xhr) { + var text; + if (xhr.status == 409) { + text = gettext("Cannot delete the default profile or the currently active profile."); + } else { + text = gettext("There was unexpected error while removing the printer profile, please consult the logs."); + } + new PNotify({title: gettext("Could not delete profile"), text: text, type: "error", hide: false}); + self.requestInProgress(false); + }); + }; + + showConfirmationDialog(_.sprintf(gettext("You are about to delete the printer profile \"%(name)s\"."), {name: data.name}), + perform); }; self.updateProfile = function(profile, callback) { @@ -517,13 +536,14 @@ $(function() { if (callback !== undefined) { callback(); } - self.requestData(); + self.requestData() + .always(function() { + self.requestInProgress(false); + }); }) .fail(function() { var text = gettext("There was unexpected error while updating the printer profile, please consult the logs."); - new PNotify({title: gettext("Saving failed"), text: text, type: "error", hide: false}); - }) - .always(function() { + new PNotify({title: gettext("Could not update profile"), text: text, type: "error", hide: false}); self.requestInProgress(false); }); }; diff --git a/src/octoprint/static/js/app/viewmodels/settings.js b/src/octoprint/static/js/app/viewmodels/settings.js index 4706cb7f87..4b805eadb9 100644 --- a/src/octoprint/static/js/app/viewmodels/settings.js +++ b/src/octoprint/static/js/app/viewmodels/settings.js @@ -91,6 +91,8 @@ $(function() { } }; + self.webcam_available_ratios = ["16:9", "4:3"]; + var auto_locale = {language: "_default", display: gettext("Autodetect from browser"), english: undefined}; self.locales = ko.observableArray([auto_locale].concat(_.sortBy(_.values(AVAILABLE_LOCALES), function(n) { return n.display; @@ -110,6 +112,7 @@ $(function() { self.printer_defaultExtrusionLength = ko.observable(undefined); self.webcam_streamUrl = ko.observable(undefined); + self.webcam_streamRatio = ko.observable(undefined); self.webcam_snapshotUrl = ko.observable(undefined); self.webcam_ffmpegPath = ko.observable(undefined); self.webcam_bitrate = ko.observable(undefined); @@ -140,6 +143,7 @@ $(function() { self.feature_firmwareDetection = ko.observable(undefined); self.feature_printCancelConfirmation = ko.observable(undefined); self.feature_blockWhileDwelling = ko.observable(undefined); + self.feature_g90InfluencesExtruder = ko.observable(undefined); self.serial_port = ko.observable(); self.serial_baudrate = ko.observable(); @@ -224,37 +228,48 @@ $(function() { self.terminalFilters.remove(filter); }; + self.testWebcamStreamUrlBusy = ko.observable(false); self.testWebcamStreamUrl = function() { if (!self.webcam_streamUrl()) { return; } + if (self.testWebcamStreamUrlBusy()) { + return; + } + var text = gettext("If you see your webcam stream below, the entered stream URL is ok."); var image = $(''); var message = $("

    ") .append(text) .append(image); + + self.testWebcamStreamUrlBusy(true); showMessageDialog({ title: gettext("Stream test"), - message: message + message: message, + onclose: function() { + self.testWebcamStreamUrlBusy(false); + } }); }; + self.testWebcamSnapshotUrlBusy = ko.observable(false); self.testWebcamSnapshotUrl = function(viewModel, event) { if (!self.webcam_snapshotUrl()) { return; } - var target = $(event.target); - target.prepend(' '); + if (self.testWebcamSnapshotUrlBusy()) { + return; + } var errorText = gettext("Could not retrieve snapshot URL, please double check the URL"); var errorTitle = gettext("Snapshot test failed"); + self.testWebcamSnapshotUrlBusy(true); OctoPrint.util.testUrl(self.webcam_snapshotUrl(), {method: "GET", response: "bytes"}) .done(function(response) { - $("i.icon-spinner", target).remove(); - if (!response.result) { showMessageDialog({ title: errorTitle, @@ -274,23 +289,34 @@ $(function() { var text = gettext("If you see your webcam snapshot picture below, the entered snapshot URL is ok."); showMessageDialog({ title: gettext("Snapshot test"), - message: $('

    ' + text + '

    ') + message: $('

    ' + text + '

    '), + onclose: function() { + self.testWebcamSnapshotUrlBusy(false); + } }); }) .fail(function() { - $("i.icon-spinner", target).remove(); showMessageDialog({ title: errorTitle, - message: errorText + message: errorText, + onclose: function() { + self.testWebcamSnapshotUrlBusy(false); + } }); }); }; + self.testWebcamFfmpegPathBusy = ko.observable(false); self.testWebcamFfmpegPath = function() { if (!self.webcam_ffmpegPath()) { return; } + if (self.testWebcamFfmpegPathBusy()) { + return; + } + + self.testWebcamFfmpegPathBusy(true); OctoPrint.util.testExecutable(self.webcam_ffmpegPath()) .done(function(response) { if (!response.result) { @@ -306,13 +332,12 @@ $(function() { } self.webcam_ffmpegPathOk(response.result); self.webcam_ffmpegPathBroken(!response.result); + }) + .always(function() { + self.testWebcamFfmpegPathBusy(false); }); }; - self.onSettingsShown = function() { - self.requestData(); - }; - self.onSettingsHidden = function() { self.webcam_ffmpegPathReset(); }; @@ -419,6 +444,19 @@ $(function() { self.settingsDialog.modal("hide"); }; + self.generateApiKey = function() { + if (!CONFIG_ACCESS_CONTROL) return; + + showConfirmationDialog(gettext("This will generate a new API Key. The old API Key will cease to function immediately."), + function() { + OctoPrint.settings.generateApiKey() + .done(function(response) { + self.api_key(response.apikey); + self.requestData(); + }); + }); + }; + self.showTranslationManager = function() { self.translationManagerDialog.modal(); return false; @@ -833,6 +871,16 @@ $(function() { // better refresh them now self.requestData(); }; + + self.onUserLoggedIn = function() { + // we might have other user rights now, refresh + self.requestData(); + }; + + self.onUserLoggedOut = function() { + // we might have other user rights now, refresh + self.requestData(); + } } OCTOPRINT_VIEWMODELS.push([ diff --git a/src/octoprint/static/js/app/viewmodels/terminal.js b/src/octoprint/static/js/app/viewmodels/terminal.js index baa58678be..4fdd47e7a3 100644 --- a/src/octoprint/static/js/app/viewmodels/terminal.js +++ b/src/octoprint/static/js/app/viewmodels/terminal.js @@ -35,8 +35,10 @@ $(function() { self.enableFancyFunctionality = ko.observable(true); self.disableTerminalLogDuringPrinting = ko.observable(false); - self.acceptableTime = 500; + + self.acceptableFancyTime = 500; self.acceptableUnfancyTime = 300; + self.reenableTimeout = 5000; self.forceFancyFunctionality = ko.observable(false); self.forceTerminalLogDuringPrinting = ko.observable(false); @@ -122,23 +124,63 @@ $(function() { self.updateFilterRegex(); }); + self._reenableFancyTimer = undefined; + self._reenableUnfancyTimer = undefined; + self._disableFancy = function(difference) { + log.warn("Terminal: Detected slow client (needed " + difference + "ms for processing new log data), disabling fancy terminal functionality"); + if (self._reenableFancyTimer) { + window.clearTimeout(self._reenableFancyTimer); + self._reenableFancyTimer = undefined; + } + self.enableFancyFunctionality(false); + }; + self._reenableFancy = function(difference) { + if (self._reenableFancyTimer) return; + self._reenableFancyTimer = window.setTimeout(function() { + log.info("Terminal: Client speed recovered, enabling fancy terminal functionality"); + self.enableFancyFunctionality(true); + }, self.reenableTimeout); + }; + self._disableUnfancy = function(difference) { + log.warn("Terminal: Detected very slow client (needed " + difference + "ms for processing new log data), completely disabling terminal output during printing"); + if (self._reenableUnfancyTimer) { + window.clearTimeout(self._reenableUnfancyTimer); + self._reenableUnfancyTimer = undefined; + } + self.disableTerminalLogDuringPrinting(true); + }; + self._reenableUnfancy = function() { + if (self._reenableUnfancyTimer) return; + self._reenableUnfancyTimer = window.setTimeout(function() { + log.info("Terminal: Client speed recovered, enabling terminal output during printing"); + self.disableTerminalLogDuringPrinting(false); + }, self.reenableTimeout); + }; + self.fromCurrentData = function(data) { self._processStateData(data.state); var start = new Date().getTime(); self._processCurrentLogData(data.logs); var end = new Date().getTime(); - var difference = end - start; + if (self.enableFancyFunctionality()) { - if (difference > self.acceptableTime) { - self.enableFancyFunctionality(false); - log.warn("Terminal: Detected slow client (needed " + difference + "ms for processing new log data), disabling fancy terminal functionality"); + // fancy enabled -> check if we need to disable fancy + if (difference >= self.acceptableFancyTime) { + self._disableFancy(difference); + } + } else if (!self.disableTerminalLogDuringPrinting()) { + // fancy disabled, unfancy not -> check if we need to disable unfancy or re-enable fancy + if (difference >= self.acceptableUnfancyTime) { + self._disableUnfancy(difference); + } else if (difference < self.acceptableFancyTime / 2.0) { + self._reenableFancy(difference); } } else { - if (!self.disableTerminalLogDuringPrinting() && difference > self.acceptableUnfancyTime) { - self.disableTerminalLogDuringPrinting(true); - log.warn("Terminal: Detected very slow client (needed " + difference + "ms for processing new log data), completely disabling terminal output during printing"); + // fancy & unfancy disabled -> check if we need to re-enable unfancy + if (difference < self.acceptableUnfancyTime / 2.0) { + self._reenableUnfancy(difference); } } }; diff --git a/src/octoprint/static/js/app/viewmodels/timelapse.js b/src/octoprint/static/js/app/viewmodels/timelapse.js index f50b6da1cb..e85447eef4 100644 --- a/src/octoprint/static/js/app/viewmodels/timelapse.js +++ b/src/octoprint/static/js/app/viewmodels/timelapse.js @@ -10,7 +10,7 @@ $(function() { self.defaultPostRoll = 0; self.defaultInterval = 10; self.defaultRetractionZHop = 0; - self.defaultCapturePostroll = true; + self.defaultCapturePostRoll = true; self.timelapseType = ko.observable(undefined); self.timelapseTimedInterval = ko.observable(self.defaultInterval); @@ -30,6 +30,13 @@ $(function() { self.isReady = ko.observable(undefined); self.isLoading = ko.observable(undefined); + self.markedForFileDeletion = ko.observableArray([]); + self.markedForUnrenderedDeletion = ko.observableArray([]); + + self.isTemporary = ko.pureComputed(function() { + return self.isDirty() && !self.persist(); + }); + self.isBusy = ko.pureComputed(function() { return self.isPrinting() || self.isPaused(); }); @@ -66,6 +73,9 @@ $(function() { self.timelapseCapturePostRoll.subscribe(function() { self.isDirty(true); }); + self.persist.subscribe(function() { + self.isDirty(true); + }); // initialize list helper self.listHelper = new ItemListHelper( @@ -138,24 +148,31 @@ $(function() { var config = response.config; if (config === undefined) return; - self.timelapseType(config.type); + // timelapses & unrendered self.listHelper.updateItems(response.files); + self.listHelper.resetPage(); if (response.unrendered) { self.unrenderedListHelper.updateItems(response.unrendered); + self.unrenderedListHelper.resetPage(); } - if (config.type == "timed") { - if (config.interval != undefined && config.interval > 0) { - self.timelapseTimedInterval(config.interval); - } + // timelapse config + self.timelapseType(config.type); + + if (config.type == "timed" && config.interval != undefined && config.interval > 0) { + self.timelapseTimedInterval(config.interval); } else { self.timelapseTimedInterval(self.defaultInterval); } - if (config.type == "zchange") { - if (config.retractionZHop != undefined && config.retractionZHop > 0) { - self.timelapseRetractionZHop(config.retractionZHop); - } + if (config.type == "timed" && config.capturePostRoll != undefined){ + self.timelapseCapturePostRoll(config.capturePostRoll); + } else { + self.timelapseCapturePostRoll(self.defaultCapturePostRoll); + } + + if (config.type == "zchange" && config.retractionZHop != undefined && config.retractionZHop > 0) { + self.timelapseRetractionZHop(config.retractionZHop); } else { self.timelapseRetractionZHop(self.defaultRetractionZHop); } @@ -172,12 +189,6 @@ $(function() { self.timelapseFps(self.defaultFps); } - if (config.capturePostRoll != undefined){ - self.timelapseCapturePostRoll(config.capturePostRoll); - } else { - self.timelapseCapturePostRoll(self.defaultCapturePostRoll); - } - self.persist(false); self.isDirty(false); }; @@ -200,14 +211,135 @@ $(function() { self.isLoading(data.flags.loading); }; + self.markFilesOnPage = function() { + self.markedForFileDeletion(_.uniq(self.markedForFileDeletion().concat(_.map(self.listHelper.paginatedItems(), "name")))); + }; + + self.markAllFiles = function() { + self.markedForFileDeletion(_.map(self.listHelper.allItems, "name")); + }; + + self.clearMarkedFiles = function() { + self.markedForFileDeletion.removeAll(); + }; + self.removeFile = function(filename) { - OctoPrint.timelapse.delete(filename) - .done(self.requestData); + var perform = function() { + OctoPrint.timelapse.delete(filename) + .done(function() { + self.markedForFileDeletion.remove(filename); + self.requestData() + }); + }; + + showConfirmationDialog(_.sprintf(gettext("You are about to delete timelapse file \"%(name)s\"."), {name: filename}), + perform) + }; + + self.removeMarkedFiles = function() { + var perform = function() { + self._bulkRemove(self.markedForFileDeletion(), "files") + .done(function() { + self.markedForFileDeletion.removeAll(); + }); + }; + + showConfirmationDialog(_.sprintf(gettext("You are about to delete %(count)d timelapse files."), {count: self.markedForFileDeletion().length}), + perform); + }; + + self.markUnrenderedOnPage = function() { + self.markedForUnrenderedDeletion(_.uniq(self.markedForUnrenderedDeletion().concat(_.map(self.unrenderedListHelper.paginatedItems(), "name")))); + }; + + self.markAllUnrendered = function() { + self.markedForUnrenderedDeletion(_.map(self.unrenderedListHelper.allItems, "name")); + }; + + self.clearMarkedUnrendered = function() { + self.markedForUnrenderedDeletion.removeAll(); }; self.removeUnrendered = function(name) { - OctoPrint.timelapse.deleteUnrendered(name) - .done(self.requestData); + var perform = function() { + OctoPrint.timelapse.deleteUnrendered(name) + .done(function() { + self.markedForUnrenderedDeletion.remove(name); + self.requestData(); + }); + }; + + showConfirmationDialog(_.sprintf(gettext("You are about to delete unrendered timelapse \"%(name)s\"."), {name: name}), + perform) + }; + + self.removeMarkedUnrendered = function() { + var perform = function() { + self._bulkRemove(self.markedForUnrenderedDeletion(), "unrendered") + .done(function() { + self.markedForUnrenderedDeletion.removeAll(); + }); + }; + + showConfirmationDialog(_.sprintf(gettext("You are about to delete %(count)d unrendered timelapses."), {count: self.markedForUnrenderedDeletion().length}), + perform); + }; + + self._bulkRemove = function(files, type) { + var title, message, handler; + + if (type == "files") { + title = gettext("Deleting timelapse files"); + message = _.sprintf(gettext("Deleting %(count)d timelapse files..."), {count: files.length}); + handler = function(filename) { + return OctoPrint.timelapse.delete(filename) + .done(function() { + deferred.notify(_.sprintf(gettext("Deleted %(filename)s..."), {filename: filename}), true); + }) + .fail(function() { + deferred.notify(_.sprintf(gettext("Deletion of %(filename)s failed, continuing..."), {filename: filename}), false); + }); + } + } else if (type == "unrendered") { + title = gettext("Deleting unrendered timelapses"); + message = _.sprintf(gettext("Deleting %(count)d unrendered timelapses..."), {count: files.length}); + handler = function(filename) { + return OctoPrint.timelapse.deleteUnrendered(filename) + .done(function() { + deferred.notify(_.sprintf(gettext("Deleted %(filename)s..."), {filename: filename}), true); + }) + .fail(function() { + deferred.notify(_.sprintf(gettext("Deletion of %(filename)s failed, continuing..."), {filename: filename}), false); + }); + } + } else { + return; + } + + var deferred = $.Deferred(); + + var promise = deferred.promise(); + + var options = { + title: title, + message: message, + max: files.length, + output: true + }; + showProgressModal(options, promise); + + var requests = []; + _.each(files, function(filename) { + var request = handler(filename); + requests.push(request) + }); + $.when.apply($, _.map(requests, wrapPromiseWithAlways)) + .done(function() { + deferred.resolve(); + self.requestData(); + }); + + return promise; }; self.renderUnrendered = function(name) { diff --git a/src/octoprint/static/js/app/viewmodels/usersettings.js b/src/octoprint/static/js/app/viewmodels/usersettings.js index b07e92f9ff..27bfbae97a 100644 --- a/src/octoprint/static/js/app/viewmodels/usersettings.js +++ b/src/octoprint/static/js/app/viewmodels/usersettings.js @@ -71,16 +71,32 @@ $(function() { self.generateApikey = function() { if (!CONFIG_ACCESS_CONTROL) return; - self.users.generateApikey(self.currentUser().name, function(response) { - self.access_apikey(response.apikey); - }); + + var generate = function() { + self.users.generateApikey(self.currentUser().name) + .done(function(response) { + self.access_apikey(response.apikey); + }); + }; + + if (self.access_apikey()) { + showConfirmationDialog(gettext("This will generate a new API Key. The old API Key will cease to function immediately."), + generate); + } else { + generate(); + } }; self.deleteApikey = function() { if (!CONFIG_ACCESS_CONTROL) return; - self.users.deleteApikey(self.currentUser().name, function() { - self.access_apikey(undefined); - }); + if (!self.access_apikey()) return; + + showConfirmationDialog(gettext("This will delete the API Key. It will cease to to function immediately."), function() { + self.users.deleteApikey(self.currentUser().name) + .done(function() { + self.access_apikey(undefined); + }); + }) }; self.updateSettings = function(username, settings) { diff --git a/src/octoprint/static/js/lib/pnotify.min.js b/src/octoprint/static/js/lib/pnotify.min.js deleted file mode 100644 index c503d66cc6..0000000000 --- a/src/octoprint/static/js/lib/pnotify.min.js +++ /dev/null @@ -1,54 +0,0 @@ -/* -PNotify 2.0.1 sciactive.com/pnotify/ -(C) 2014 Hunter Perrin -license GPL/LGPL/MPL -*/ -(function(c){"function"===typeof define&&define.amd?define("pnotify",["jquery"],c):c(jQuery)})(function(c){var p={dir1:"down",dir2:"left",push:"bottom",spacing1:25,spacing2:25,context:c("body")},f,g,h=c(window),m=function(){g=c("body");PNotify.prototype.options.stack.context=g;h=c(window);h.bind("resize",function(){f&&clearTimeout(f);f=setTimeout(function(){PNotify.positionAll(!0)},10)})};PNotify=function(b){this.parseOptions(b);this.init()};c.extend(PNotify.prototype,{version:"2.0.1",options:{title:!1, -title_escape:!1,text:!1,text_escape:!1,styling:"bootstrap3",addclass:"",cornerclass:"",auto_display:!0,width:"300px",min_height:"16px",type:"notice",icon:!0,opacity:1,animation:"fade",animate_speed:"slow",position_animate_speed:500,shadow:!0,hide:!0,delay:8E3,mouse_reset:!0,remove:!0,insert_brs:!0,destroy:!0,stack:p},modules:{},runModules:function(b,a){var c,e;for(e in this.modules)if(c="object"===typeof a&&e in a?a[e]:a,"function"===typeof this.modules[e][b])this.modules[e][b](this,"object"===typeof this.options[e]? -this.options[e]:{},c)},state:"initializing",timer:null,styles:null,elem:null,container:null,title_container:null,text_container:null,animating:!1,timerHide:!1,init:function(){var b=this;this.modules={};c.extend(!0,this.modules,PNotify.prototype.modules);this.styles="object"===typeof this.options.styling?this.options.styling:PNotify.styling[this.options.styling];this.elem=c("
    ",{"class":"ui-pnotify "+this.options.addclass,css:{display:"none"},mouseenter:function(a){if(b.options.mouse_reset&& -"out"===b.animating){if(!b.timerHide)return;b.cancelRemove()}b.options.hide&&b.options.mouse_reset&&b.cancelRemove()},mouseleave:function(a){b.options.hide&&b.options.mouse_reset&&b.queueRemove();PNotify.positionAll()}});this.container=c("
    ",{"class":this.styles.container+" ui-pnotify-container "+("error"===this.options.type?this.styles.error:"info"===this.options.type?this.styles.info:"success"===this.options.type?this.styles.success:this.styles.notice)}).appendTo(this.elem);""!==this.options.cornerclass&& -this.container.removeClass("ui-corner-all").addClass(this.options.cornerclass);this.options.shadow&&this.container.addClass("ui-pnotify-shadow");!1!==this.options.icon&&c("
    ",{"class":"ui-pnotify-icon"}).append(c("",{"class":!0===this.options.icon?"error"===this.options.type?this.styles.error_icon:"info"===this.options.type?this.styles.info_icon:"success"===this.options.type?this.styles.success_icon:this.styles.notice_icon:this.options.icon})).prependTo(this.container);this.title_container= -c("

    ",{"class":"ui-pnotify-title"}).appendTo(this.container);!1===this.options.title?this.title_container.hide():this.options.title_escape?this.title_container.text(this.options.title):this.title_container.html(this.options.title);this.text_container=c("
    ",{"class":"ui-pnotify-text"}).appendTo(this.container);!1===this.options.text?this.text_container.hide():this.options.text_escape?this.text_container.text(this.options.text):this.text_container.html(this.options.insert_brs?String(this.options.text).replace(/\n/g, -"
    "):this.options.text);"string"===typeof this.options.width&&this.elem.css("width",this.options.width);"string"===typeof this.options.min_height&&this.container.css("min-height",this.options.min_height);PNotify.notices="top"===this.options.stack.push?c.merge([this],PNotify.notices):c.merge(PNotify.notices,[this]);"top"===this.options.stack.push&&this.queuePosition(!1,1);this.options.stack.animation=!1;this.runModules("init");this.options.auto_display&&this.open();return this},update:function(b){var a= -this.options;this.parseOptions(a,b);this.options.cornerclass!==a.cornerclass&&this.container.removeClass("ui-corner-all "+a.cornerclass).addClass(this.options.cornerclass);this.options.shadow!==a.shadow&&(this.options.shadow?this.container.addClass("ui-pnotify-shadow"):this.container.removeClass("ui-pnotify-shadow"));!1===this.options.addclass?this.elem.removeClass(a.addclass):this.options.addclass!==a.addclass&&this.elem.removeClass(a.addclass).addClass(this.options.addclass);!1===this.options.title? -this.title_container.slideUp("fast"):this.options.title!==a.title&&(this.options.title_escape?this.title_container.text(this.options.title):this.title_container.html(this.options.title),!1===a.title&&this.title_container.slideDown(200));!1===this.options.text?this.text_container.slideUp("fast"):this.options.text!==a.text&&(this.options.text_escape?this.text_container.text(this.options.text):this.text_container.html(this.options.insert_brs?String(this.options.text).replace(/\n/g,"
    "):this.options.text), -!1===a.text&&this.text_container.slideDown(200));this.options.type!==a.type&&this.container.removeClass(this.styles.error+" "+this.styles.notice+" "+this.styles.success+" "+this.styles.info).addClass("error"===this.options.type?this.styles.error:"info"===this.options.type?this.styles.info:"success"===this.options.type?this.styles.success:this.styles.notice);if(this.options.icon!==a.icon||!0===this.options.icon&&this.options.type!==a.type)this.container.find("div.ui-pnotify-icon").remove(),!1!==this.options.icon&& -c("
    ",{"class":"ui-pnotify-icon"}).append(c("",{"class":!0===this.options.icon?"error"===this.options.type?this.styles.error_icon:"info"===this.options.type?this.styles.info_icon:"success"===this.options.type?this.styles.success_icon:this.styles.notice_icon:this.options.icon})).prependTo(this.container);this.options.width!==a.width&&this.elem.animate({width:this.options.width});this.options.min_height!==a.min_height&&this.container.animate({minHeight:this.options.min_height});this.options.opacity!== -a.opacity&&this.elem.fadeTo(this.options.animate_speed,this.options.opacity);this.options.hide?a.hide||this.queueRemove():this.cancelRemove();this.queuePosition(!0);this.runModules("update",a);return this},open:function(){this.state="opening";this.runModules("beforeOpen");var b=this;this.elem.parent().length||this.elem.appendTo(this.options.stack.context?this.options.stack.context:g);"top"!==this.options.stack.push&&this.position(!0);"fade"===this.options.animation||"fade"===this.options.animation.effect_in? -this.elem.show().fadeTo(0,0).hide():1!==this.options.opacity&&this.elem.show().fadeTo(0,this.options.opacity).hide();this.animateIn(function(){b.queuePosition(!0);b.options.hide&&b.queueRemove();b.state="open";b.runModules("afterOpen")});return this},remove:function(b){this.state="closing";this.timerHide=!!b;this.runModules("beforeClose");var a=this;this.timer&&(window.clearTimeout(this.timer),this.timer=null);this.animateOut(function(){a.state="closed";a.runModules("afterClose");a.queuePosition(!0); -a.options.remove&&a.elem.detach();a.runModules("beforeDestroy");if(a.options.destroy&&null!==PNotify.notices){var b=c.inArray(a,PNotify.notices);-1!==b&&PNotify.notices.splice(b,1)}a.runModules("afterDestroy")});return this},get:function(){return this.elem},parseOptions:function(b,a){this.options=c.extend(!0,{},PNotify.prototype.options);this.options.stack=PNotify.prototype.options.stack;var n=[b,a],e,f;for(f in n){e=n[f];if("undefined"==typeof e)break;if("object"!==typeof e)this.options.text=e;else for(var d in e)this.modules[d]? -c.extend(!0,this.options[d],e[d]):this.options[d]=e[d]}},animateIn:function(b){this.animating="in";var a;a="undefined"!==typeof this.options.animation.effect_in?this.options.animation.effect_in:this.options.animation;"none"===a?(this.elem.show(),b()):"show"===a?this.elem.show(this.options.animate_speed,b):"fade"===a?this.elem.show().fadeTo(this.options.animate_speed,this.options.opacity,b):"slide"===a?this.elem.slideDown(this.options.animate_speed,b):"function"===typeof a?a("in",b,this.elem):this.elem.show(a, -"object"===typeof this.options.animation.options_in?this.options.animation.options_in:{},this.options.animate_speed,b);this.elem.parent().hasClass("ui-effects-wrapper")&&this.elem.parent().css({position:"fixed",overflow:"visible"});"slide"!==a&&this.elem.css("overflow","visible");this.container.css("overflow","hidden")},animateOut:function(b){this.animating="out";var a;a="undefined"!==typeof this.options.animation.effect_out?this.options.animation.effect_out:this.options.animation;"none"===a?(this.elem.hide(), -b()):"show"===a?this.elem.hide(this.options.animate_speed,b):"fade"===a?this.elem.fadeOut(this.options.animate_speed,b):"slide"===a?this.elem.slideUp(this.options.animate_speed,b):"function"===typeof a?a("out",b,this.elem):this.elem.hide(a,"object"===typeof this.options.animation.options_out?this.options.animation.options_out:{},this.options.animate_speed,b);this.elem.parent().hasClass("ui-effects-wrapper")&&this.elem.parent().css({position:"fixed",overflow:"visible"});"slide"!==a&&this.elem.css("overflow", -"visible");this.container.css("overflow","hidden")},position:function(b){var a=this.options.stack,c=this.elem;c.parent().hasClass("ui-effects-wrapper")&&(c=this.elem.css({left:"0",top:"0",right:"0",bottom:"0"}).parent());"undefined"===typeof a.context&&(a.context=g);if(a){"number"!==typeof a.nextpos1&&(a.nextpos1=a.firstpos1);"number"!==typeof a.nextpos2&&(a.nextpos2=a.firstpos2);"number"!==typeof a.addpos2&&(a.addpos2=0);var e="none"===c.css("display");if(!e||b){var f,d={},k;switch(a.dir1){case "down":k= -"top";break;case "up":k="bottom";break;case "left":k="right";break;case "right":k="left"}b=parseInt(c.css(k).replace(/(?:\..*|[^0-9.])/g,""));isNaN(b)&&(b=0);"undefined"!==typeof a.firstpos1||e||(a.firstpos1=b,a.nextpos1=a.firstpos1);var l;switch(a.dir2){case "down":l="top";break;case "up":l="bottom";break;case "left":l="right";break;case "right":l="left"}f=parseInt(c.css(l).replace(/(?:\..*|[^0-9.])/g,""));isNaN(f)&&(f=0);"undefined"!==typeof a.firstpos2||e||(a.firstpos2=f,a.nextpos2=a.firstpos2); -if("down"===a.dir1&&a.nextpos1+c.height()>(a.context.is(g)?h.height():a.context.prop("scrollHeight"))||"up"===a.dir1&&a.nextpos1+c.height()>(a.context.is(g)?h.height():a.context.prop("scrollHeight"))||"left"===a.dir1&&a.nextpos1+c.width()>(a.context.is(g)?h.width():a.context.prop("scrollWidth"))||"right"===a.dir1&&a.nextpos1+c.width()>(a.context.is(g)?h.width():a.context.prop("scrollWidth")))a.nextpos1=a.firstpos1,a.nextpos2+=a.addpos2+("undefined"===typeof a.spacing2?25:a.spacing2),a.addpos2=0;if(a.animation&& -a.nextpos2a.addpos2&&(a.addpos2=c.height());break;case "left":case "right":c.outerWidth(!0)>a.addpos2&&(a.addpos2=c.width())}if("number"===typeof a.nextpos1)if(a.animation&&(b>a.nextpos1||d.top||d.bottom||d.right|| -d.left))switch(a.dir1){case "down":d.top=a.nextpos1+"px";break;case "up":d.bottom=a.nextpos1+"px";break;case "left":d.right=a.nextpos1+"px";break;case "right":d.left=a.nextpos1+"px"}else c.css(k,a.nextpos1+"px");(d.top||d.bottom||d.right||d.left)&&c.animate(d,{duration:this.options.position_animate_speed,queue:!1});switch(a.dir1){case "down":case "up":a.nextpos1+=c.height()+("undefined"===typeof a.spacing1?25:a.spacing1);break;case "left":case "right":a.nextpos1+=c.width()+("undefined"===typeof a.spacing1? -25:a.spacing1)}}return this}},queuePosition:function(b,a){f&&clearTimeout(f);a||(a=10);f=setTimeout(function(){PNotify.positionAll(b)},a);return this},cancelRemove:function(){this.timer&&window.clearTimeout(this.timer);"closing"===this.state&&(this.elem.stop(!0),this.state="open",this.animating="in",this.elem.css("height","auto").animate({width:this.options.width,opacity:this.options.opacity},"fast"));return this},queueRemove:function(){var b=this;this.cancelRemove();this.timer=window.setTimeout(function(){b.remove(!0)}, -isNaN(this.options.delay)?0:this.options.delay);return this}});c.extend(PNotify,{notices:[],removeAll:function(){c.each(PNotify.notices,function(){this.remove&&this.remove()})},positionAll:function(b){f&&clearTimeout(f);f=null;c.each(PNotify.notices,function(){var a=this.options.stack;a&&(a.nextpos1=a.firstpos1,a.nextpos2=a.firstpos2,a.addpos2=0,a.animation=b)});c.each(PNotify.notices,function(){this.position()})},styling:{jqueryui:{container:"ui-widget ui-widget-content ui-corner-all",notice:"ui-state-highlight", -notice_icon:"ui-icon ui-icon-info",info:"",info_icon:"ui-icon ui-icon-info",success:"ui-state-default",success_icon:"ui-icon ui-icon-circle-check",error:"ui-state-error",error_icon:"ui-icon ui-icon-alert"},bootstrap2:{container:"alert",notice:"",notice_icon:"icon-exclamation-sign",info:"alert-info",info_icon:"icon-info-sign",success:"alert-success",success_icon:"icon-ok-sign",error:"alert-error",error_icon:"icon-warning-sign"},bootstrap3:{container:"alert",notice:"alert-warning",notice_icon:"glyphicon glyphicon-exclamation-sign", -info:"alert-info",info_icon:"glyphicon glyphicon-info-sign",success:"alert-success",success_icon:"glyphicon glyphicon-ok-sign",error:"alert-danger",error_icon:"glyphicon glyphicon-warning-sign"}}});PNotify.styling.fontawesome=c.extend({},PNotify.styling.bootstrap3);c.extend(PNotify.styling.fontawesome,{notice_icon:"fa fa-exclamation-circle",info_icon:"fa fa-info",success_icon:"fa fa-check",error_icon:"fa fa-warning"});document.body?m():c(m);return PNotify}); -(function(c){"function"===typeof define&&define.amd?define("pnotify.buttons",["jquery","pnotify"],c):c(jQuery,PNotify)})(function(c,e){e.prototype.options.buttons={closer:!0,closer_hover:!0,sticker:!0,sticker_hover:!0,labels:{close:"Close",stick:"Stick"}};e.prototype.modules.buttons={myOptions:null,closer:null,sticker:null,init:function(a,b){var d=this;this.myOptions=b;a.elem.on({mouseenter:function(b){!d.myOptions.sticker||a.options.nonblock&&a.options.nonblock.nonblock||d.sticker.trigger("pnotify_icon").css("visibility", -"visible");!d.myOptions.closer||a.options.nonblock&&a.options.nonblock.nonblock||d.closer.css("visibility","visible")},mouseleave:function(a){d.myOptions.sticker_hover&&d.sticker.css("visibility","hidden");d.myOptions.closer_hover&&d.closer.css("visibility","hidden")}});this.sticker=c("
    ",{"class":"ui-pnotify-sticker",css:{cursor:"pointer",visibility:b.sticker_hover?"hidden":"visible"},click:function(){a.options.hide=!a.options.hide;a.options.hide?a.queueRemove():a.cancelRemove();c(this).trigger("pnotify_icon")}}).bind("pnotify_icon", -function(){c(this).children().removeClass(a.styles.pin_up+" "+a.styles.pin_down).addClass(a.options.hide?a.styles.pin_up:a.styles.pin_down)}).append(c("",{"class":a.styles.pin_up,title:b.labels.stick})).prependTo(a.container);(!b.sticker||a.options.nonblock&&a.options.nonblock.nonblock)&&this.sticker.css("display","none");this.closer=c("
    ",{"class":"ui-pnotify-closer",css:{cursor:"pointer",visibility:b.closer_hover?"hidden":"visible"},click:function(){a.remove(!1);d.sticker.css("visibility", -"hidden");d.closer.css("visibility","hidden")}}).append(c("",{"class":a.styles.closer,title:b.labels.close})).prependTo(a.container);(!b.closer||a.options.nonblock&&a.options.nonblock.nonblock)&&this.closer.css("display","none")},update:function(a,b){this.myOptions=b;!b.closer||a.options.nonblock&&a.options.nonblock.nonblock?this.closer.css("display","none"):b.closer&&this.closer.css("display","block");!b.sticker||a.options.nonblock&&a.options.nonblock.nonblock?this.sticker.css("display", -"none"):b.sticker&&this.sticker.css("display","block");this.sticker.trigger("pnotify_icon");b.sticker_hover?this.sticker.css("visibility","hidden"):a.options.nonblock&&a.options.nonblock.nonblock||this.sticker.css("visibility","visible");b.closer_hover?this.closer.css("visibility","hidden"):a.options.nonblock&&a.options.nonblock.nonblock||this.closer.css("visibility","visible")}};c.extend(e.styling.jqueryui,{closer:"ui-icon ui-icon-close",pin_up:"ui-icon ui-icon-pin-w",pin_down:"ui-icon ui-icon-pin-s"}); -c.extend(e.styling.bootstrap2,{closer:"icon-remove",pin_up:"icon-pause",pin_down:"icon-play"});c.extend(e.styling.bootstrap3,{closer:"glyphicon glyphicon-remove",pin_up:"glyphicon glyphicon-pause",pin_down:"glyphicon glyphicon-play"});c.extend(e.styling.fontawesome,{closer:"fa fa-times",pin_up:"fa fa-pause",pin_down:"fa fa-play"})}); -(function(d){"function"===typeof define&&define.amd?define("pnotify.confirm",["jquery","pnotify"],d):d(jQuery,PNotify)})(function(d,e){e.prototype.options.confirm={confirm:!1,prompt:!1,prompt_class:"",prompt_default:"",prompt_multi_line:!1,align:"right",buttons:[{text:"Ok",addClass:"",promptTrigger:!0,click:function(b,a){b.remove();b.get().trigger("pnotify.confirm",[b,a])}},{text:"Cancel",addClass:"",click:function(b){b.remove();b.get().trigger("pnotify.cancel",b)}}]};e.prototype.modules.confirm= -{container:null,prompt:null,init:function(b,a){this.container=d('
    ').css("text-align",a.align).appendTo(b.container);a.confirm||a.prompt?this.makeDialog(b,a):this.container.hide()},update:function(b,a){a.confirm?(this.makeDialog(b,a),this.container.show()):this.container.hide().empty()},afterOpen:function(b,a){a.prompt&&this.prompt.focus()},makeDialog:function(b,a){var e=!1,h=this,f,c;this.container.empty();a.prompt&&(this.prompt=d("<"+(a.prompt_multi_line? -'textarea rows="5"':'input type="text"')+' style="margin-bottom:5px;clear:both;" />').addClass(b.styles.input+" "+a.prompt_class).val(a.prompt_default).appendTo(this.container));for(var k in a.buttons){f=a.buttons[k];e?this.container.append(" "):e=!0;c=d('").appendTo(b.container),b.container.append('
    '),b.elem.on({mouseenter:function(b){d.thingElem.prop("disabled",!1)},mouseleave:function(b){d.thingElem.prop("disabled",!0)}}),this.thingElem.on("click",function(){var a=0,c=setInterval(function(){a+=10;360==a&&(a=0,clearInterval(c));b.elem.css({"-moz-transform":"rotate("+a+"deg)","-webkit-transform":"rotate("+a+"deg)","-o-transform":"rotate("+a+"deg)","-ms-transform":"rotate("+ +a+"deg)",filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation="+a/360*4+")"})},20)}))},update:function(b,a,c){a.putThing&&this.thingElem?this.thingElem.show():!a.putThing&&this.thingElem&&this.thingElem.hide();this.thingElem&&this.thingElem.find("i").attr("class",b.styles.athing)},beforeOpen:function(b,a){},afterOpen:function(b,a){},beforeClose:function(b,a){},afterClose:function(b,a){},beforeDestroy:function(b,a){},afterDestroy:function(b,a){}};c.extend(d.styling.jqueryui,{athing:"ui-icon ui-icon-refresh"}); +c.extend(d.styling.bootstrap2,{athing:"icon-refresh"});c.extend(d.styling.bootstrap3,{athing:"glyphicon glyphicon-refresh"});c.extend(d.styling.fontawesome,{athing:"fa fa-refresh"})}); diff --git a/src/octoprint/static/js/lib/pnotify/pnotify.tooltip.min.js b/src/octoprint/static/js/lib/pnotify/pnotify.tooltip.min.js new file mode 100644 index 0000000000..ccdfe96c7d --- /dev/null +++ b/src/octoprint/static/js/lib/pnotify/pnotify.tooltip.min.js @@ -0,0 +1 @@ +(function(a){"object"===typeof exports&&"undefined"!==typeof module?module.exports=a(require("jquery"),require("pnotify")):"function"===typeof define&&define.amd?define("pnotify.tooltip",["jquery","pnotify"],a):a(jQuery,PNotify)})(function(a,b){}); diff --git a/src/octoprint/static/less/octoprint.less b/src/octoprint/static/less/octoprint.less index 22dc57f36e..369d64f459 100644 --- a/src/octoprint/static/less/octoprint.less +++ b/src/octoprint/static/less/octoprint.less @@ -311,6 +311,16 @@ table { } // timelapse files + &.timelapse_files_checkbox, + &.timelapse_unrendered_checkbox { + text-align: center; + width: 10px; + + input[type="checkbox"] { + margin-top: 0; + } + } + &.timelapse_files_name, &.timelapse_unrendered_name { text-overflow: ellipsis; @@ -481,7 +491,7 @@ ul.dropdown-menu li a { position: relative; outline: none; - //min-height: 440px; + background-color: black; .keycontrol_overlay { position: absolute; @@ -516,6 +526,84 @@ ul.dropdown-menu li a { float: left; } } + + .nowebcam { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + + .text { + color: white; + text-align: center; + position: relative; + margin: auto; + width: 80%; + top: 50%; + transform: translateY(-50%); + display: block; + + &.webcam_loading { + animation: pulsate 3s ease-out; + animation-iteration-count: infinite; + } + } + } + + .webcam_rotated { + position: relative; + width: 100%; + padding-bottom: 100%; + + .webcam_fixed_ratio { + position: absolute; + transform: rotate(-90deg); + top: 0; + bottom: 0; + + .webcam_fixed_ratio_inner { + width: 100%; + height: 100%; + } + } + } + + .webcam_unrotated { + .webcam_fixed_ratio { + width: 100%; + padding-bottom: 100%; + + &.ratio43 { + padding-bottom: 75%; + } + + &.ratio169 { + padding-bottom: 56.25%; + } + + &.ratio1610 { + padding-bottom: 62.5%; + } + + position: relative; + + .webcam_fixed_ratio_inner { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + } + } + } + + + img { + width: 100%; + height: 100%; + object-fit: contain; + } } /** State sidebar panel */ @@ -747,6 +835,10 @@ ul.dropdown-menu li a { #terminal-sendpanel { text-align: right; } + + #terminal-output span { + display: block; + } } #settings_dialog { @@ -857,6 +949,18 @@ textarea.block { width: 100%; } +@keyframes pulsate { + 0% { + opacity: 0.5; + } + 50% { + opacity: 1.0; + } + 100% { + opacity: 0.5; + } +} + #drop_overlay { position: fixed; top: 0; @@ -1021,6 +1125,15 @@ textarea.block { text-decoration: underline; } +.btn-mini .caret, .btn-small .caret { + margin-top: 8px; +} + +.dropdown-menu-right { + right: 0; + left: auto; +} + /** Styles for Bootstrap Slider */ .slider { diff --git a/src/octoprint/templates/dialogs/settings/api.jinja2 b/src/octoprint/templates/dialogs/settings/api.jinja2 index a2c99b919c..8237476aea 100644 --- a/src/octoprint/templates/dialogs/settings/api.jinja2 +++ b/src/octoprint/templates/dialogs/settings/api.jinja2 @@ -16,10 +16,14 @@
    - +
    + + +
    + {{ _('Please note that changes to the API key are applied immediately, without having to "Save" first.') }}
    -
    +
    diff --git a/src/octoprint/templates/dialogs/settings/appearance.jinja2 b/src/octoprint/templates/dialogs/settings/appearance.jinja2 index c4a95ea434..3a03c562e3 100644 --- a/src/octoprint/templates/dialogs/settings/appearance.jinja2 +++ b/src/octoprint/templates/dialogs/settings/appearance.jinja2 @@ -39,7 +39,7 @@
    @@ -51,6 +51,13 @@

    {{ _('Manage Language Packs...') }}

    +
    +
    + +
    +
    diff --git a/src/octoprint/templates/dialogs/settings/webcam.jinja2 b/src/octoprint/templates/dialogs/settings/webcam.jinja2 index 97d5924bfb..5b86f7bc9c 100644 --- a/src/octoprint/templates/dialogs/settings/webcam.jinja2 +++ b/src/octoprint/templates/dialogs/settings/webcam.jinja2 @@ -2,6 +2,7 @@
    {% include "snippets/settings/webcam/webcamStreamUrl.jinja2" %} + {% include "snippets/settings/webcam/webcamStreamRatio.jinja2" %} {% include "snippets/settings/webcam/webcamOrientation.jinja2" %}
    diff --git a/src/octoprint/templates/dialogs/usersettings/access.jinja2 b/src/octoprint/templates/dialogs/usersettings/access.jinja2 index 6cb7e2f9ea..67dee1f954 100644 --- a/src/octoprint/templates/dialogs/usersettings/access.jinja2 +++ b/src/octoprint/templates/dialogs/usersettings/access.jinja2 @@ -24,14 +24,14 @@
    - - - + + +
    - + {{ _('Please note that changes to the API key are applied immediately, without having to "Confirm" first.') }}
    -
    +
    diff --git a/src/octoprint/templates/overlays/reloadui.jinja2 b/src/octoprint/templates/overlays/reloadui.jinja2 index 746c91298b..c0614dea48 100644 --- a/src/octoprint/templates/overlays/reloadui.jinja2 +++ b/src/octoprint/templates/overlays/reloadui.jinja2 @@ -11,7 +11,7 @@ web interface now by clicking the button below. {% endtrans %}

    - {{ _('Reload now') }} +

    diff --git a/src/octoprint/templates/sidebar/files.jinja2 b/src/octoprint/templates/sidebar/files.jinja2 index 3fde22c9ec..99b199df8a 100644 --- a/src/octoprint/templates/sidebar/files.jinja2 +++ b/src/octoprint/templates/sidebar/files.jinja2 @@ -21,7 +21,7 @@
    -
    +
    diff --git a/src/octoprint/templates/sidebar/files_header.jinja2 b/src/octoprint/templates/sidebar/files_header.jinja2 index a59550834c..faafada70b 100644 --- a/src/octoprint/templates/sidebar/files_header.jinja2 +++ b/src/octoprint/templates/sidebar/files_header.jinja2 @@ -23,7 +23,6 @@
  • {{ _('Only show files stored on SD') }}
  • {% endif %}
  • -
  • {{ _('Hide folders without files') }}
  • {{ _('Hide successfully printed files') }}
diff --git a/src/octoprint/templates/snippets/settings/printerprofiles/profiles.jinja2 b/src/octoprint/templates/snippets/settings/printerprofiles/profiles.jinja2 index 643458be91..7c8520a627 100644 --- a/src/octoprint/templates/snippets/settings/printerprofiles/profiles.jinja2 +++ b/src/octoprint/templates/snippets/settings/printerprofiles/profiles.jinja2 @@ -8,10 +8,10 @@ - + -  |  |  +  |  |  diff --git a/src/octoprint/templates/snippets/settings/webcam/ffmpegPath.jinja2 b/src/octoprint/templates/snippets/settings/webcam/ffmpegPath.jinja2 index 14375942ac..478e616616 100644 --- a/src/octoprint/templates/snippets/settings/webcam/ffmpegPath.jinja2 +++ b/src/octoprint/templates/snippets/settings/webcam/ffmpegPath.jinja2 @@ -3,7 +3,7 @@
- +
diff --git a/src/octoprint/templates/snippets/settings/webcam/webcamSnapshotUrl.jinja2 b/src/octoprint/templates/snippets/settings/webcam/webcamSnapshotUrl.jinja2 index da33fc48f6..cf14954068 100644 --- a/src/octoprint/templates/snippets/settings/webcam/webcamSnapshotUrl.jinja2 +++ b/src/octoprint/templates/snippets/settings/webcam/webcamSnapshotUrl.jinja2 @@ -3,7 +3,7 @@
- +
{% trans %}Fully qualified URL, needs to be reachable by OctoPrint's server{% endtrans %}
diff --git a/src/octoprint/templates/snippets/settings/webcam/webcamStreamRatio.jinja2 b/src/octoprint/templates/snippets/settings/webcam/webcamStreamRatio.jinja2 new file mode 100644 index 0000000000..ee5cdd1dc3 --- /dev/null +++ b/src/octoprint/templates/snippets/settings/webcam/webcamStreamRatio.jinja2 @@ -0,0 +1,7 @@ +
+ +
+ + {% trans %}If the stream has a different aspect ratio than configured here it will be letterboxed.{% endtrans %} +
+
diff --git a/src/octoprint/templates/snippets/settings/webcam/webcamStreamUrl.jinja2 b/src/octoprint/templates/snippets/settings/webcam/webcamStreamUrl.jinja2 index a26337fa5f..42aaa5296c 100644 --- a/src/octoprint/templates/snippets/settings/webcam/webcamStreamUrl.jinja2 +++ b/src/octoprint/templates/snippets/settings/webcam/webcamStreamUrl.jinja2 @@ -3,7 +3,7 @@
- +
{% trans %}Needs to be reachable from the browser displaying the OctoPrint UI, used to embed the webcam stream into the page.{% endtrans %}
diff --git a/src/octoprint/templates/tabs/control.jinja2 b/src/octoprint/templates/tabs/control.jinja2 index 93d6e5246a..b3b69e477e 100644 --- a/src/octoprint/templates/tabs/control.jinja2 +++ b/src/octoprint/templates/tabs/control.jinja2 @@ -1,7 +1,20 @@ {% if webcamStream %}
-
- +
+
+

{{ _('Webcam stream loading...') }}

+
+
+

{{ _('Webcam stream not loaded') }}

+

{{ _('It might not be correctly configured. You can change the URL of the stream under "Settings" > "Webcam & Timelapse" > "Stream URL". If you don\'t have a webcam just set the URL to an empty value.') }}

+
+
+
+
+
+ +
+
{{ _("Keyboard controls active") }}
diff --git a/src/octoprint/templates/tabs/terminal.jinja2 b/src/octoprint/templates/tabs/terminal.jinja2 index e826de000b..c258c978ee 100644 --- a/src/octoprint/templates/tabs/terminal.jinja2 +++ b/src/octoprint/templates/tabs/terminal.jinja2 @@ -1,5 +1,5 @@
-

+
{{ _("Scroll to end") }} | {{ _("Select all") }} @@ -16,7 +16,7 @@