Merge remote-tracking branch 'origin/maintenance' into maintenance
This commit is contained in:
commit
660de61997
67 changed files with 3629 additions and 1268 deletions
|
|
@ -10,13 +10,14 @@
|
|||
# master shall not use the lookup table, only tags
|
||||
master
|
||||
|
||||
# maintenance is currently the branch for preparation of maintenance release 1.2.8
|
||||
# so are any fix/... branches
|
||||
maintenance 1.2.8 6c622f7c4332b71c6ece59552ffc87c146155c84 pep440-dev
|
||||
fix/.* 1.2.8 6c622f7c4332b71c6ece59552ffc87c146155c84 pep440-dev
|
||||
|
||||
# Special case disconnected checkouts, e.g. 'git checkout <tag>'
|
||||
# neither should disconnected checkouts, e.g. 'git checkout <tag>'
|
||||
HEAD
|
||||
\(detached.*
|
||||
|
||||
# maintenance is currently the branch for preparation of maintenance release 1.2.9
|
||||
# so are any fix/... branches
|
||||
maintenance 1.2.9 dedadbc9ac0305799e94ae279d3bca131629c4c5 pep440-dev
|
||||
fix/.* 1.2.9 dedadbc9ac0305799e94ae279d3bca131629c4c5 pep440-dev
|
||||
|
||||
# every other branch is a development branch and thus gets resolved to 1.3.0-dev for now
|
||||
.* 1.3.0 198d3450d94be1a2 pep440-dev
|
||||
|
|
|
|||
|
|
@ -53,7 +53,9 @@ date of first contribution):
|
|||
* [Andrew Erickson](https://github.com/aerickson)
|
||||
* [Nicanor Romero Venier](https://github.com/nicanor-romero)
|
||||
* [Thomas Hou](https://github.com/masterhou)
|
||||
* [Mark Bastiaans](https://github.com/markbastiaans)
|
||||
* [Kevin Murphy](https://github.com/kevingelion)
|
||||
* [Richard Joyce](https://github.com/richjoyce)
|
||||
|
||||
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
|
||||
|
|
|
|||
125
CHANGELOG.md
125
CHANGELOG.md
|
|
@ -1,5 +1,128 @@
|
|||
# OctoPrint Changelog
|
||||
|
||||
## 1.2.8 (2015-12-07)
|
||||
|
||||
### Notes for Upgraders
|
||||
|
||||
#### A bug in 1.2.7 prevents directly updating to 1.2.8, here's what to do
|
||||
|
||||
A bug in OctoPrint 1.2.7 (fixed in 1.2.8) prevents updating OctoPrint to version
|
||||
1.2.8. If you try to perform the update, you will simply be told that "the update
|
||||
was successful", but the update won't actually have taken place. To solve this
|
||||
hen-egg-problem, a plugin has been made available that fixes said bug (through
|
||||
monkey patching).
|
||||
|
||||
The plugin is called "Updatefix 1.2.7" and can be found
|
||||
[in the plugin repository](http://plugins.octoprint.org/plugins/updatefix127/)
|
||||
and [on Github](https://github.com/OctoPrint/OctoPrint-Updatefix-1.2.7/).
|
||||
|
||||
Before attempting to update your installation from version 1.2.7 to version 1.2.8,
|
||||
please install the plugin via your plugin manager and restart your server. Note that
|
||||
you will only see it in the Plugin Manager if you need it, since it's only compatible with
|
||||
OctoPrint version 1.2.7. After you installed the plugin and restarted your server
|
||||
you can update as usual. The plugin will self-uninstall once it detects that it's
|
||||
running under OctoPrint 1.2.8. After the self-uninstall another restart of your server
|
||||
will be triggered (if you have setup your server's restart command, defaults to
|
||||
`sudo service octoprint restart` on OctoPi) in order to really get rid of any
|
||||
left-overs, so don't be alarmed when that happens, it is intentional.
|
||||
|
||||
**If you cannot or don't want to use the plugin**, alternatively you can switch
|
||||
OctoPrint to "Commit" based tracking via the settings of the Software Update plugin,
|
||||
update, then switch back to "Release" based tracking (see [this screenshot](https://i.imgur.com/wvkgiGJ.png)).
|
||||
|
||||
#### Bed temperatures are now only displayed if printer profile has a heated bed configured
|
||||
|
||||
This release fixes a [bug](https://github.com/foosel/OctoPrint/issues/1125)
|
||||
that caused bed temperature display and controls to be available even if the
|
||||
selected printer profile didn't have a heated bed configured.
|
||||
|
||||
If your printer does have a heated bed but you are not seeing its temperature
|
||||
in the "Temperature" tab after updating to 1.2.8, please make sure to check
|
||||
the "Heated Bed" option in your printer profile (under Settings > Printer Profiles)
|
||||
as shown [in this short GIF](http://i.imgur.com/wp1j9bs.gif).
|
||||
|
||||
### Improvements
|
||||
|
||||
* Version numbering now follows [PEP440](https://www.python.org/dev/peps/pep-0440/).
|
||||
* Prepared some things for publishing OctoPrint on [PyPi](https://pypi.python.org/pypi)
|
||||
in the future.
|
||||
* [BlueprintPlugin mixin](http://docs.octoprint.org/en/master/plugins/mixins.html#blueprintplugin)
|
||||
now has an `errorhandler` decorator that serves the same purpose as
|
||||
[Flask's](http://flask.pocoo.org/docs/0.10/patterns/errorpages/#error-handlers)
|
||||
([#1059](https://github.com/foosel/OctoPrint/pull/1059))
|
||||
* Interpret `M25` in a GCODE file that is being streamed from OctoPrint as
|
||||
indication to pause, like `M0` and `M1`.
|
||||
* Cache rendered page and translation files indefinitely. That should
|
||||
significantly improve performance on reloads of the web interface.
|
||||
* Added the string "unknown command" to the list of ignored printer errors.
|
||||
This should help with general firmware compatibility in case a firmware
|
||||
lacks features.
|
||||
* Added the strings "cannot open" and "cannot enter" to the list of ignored
|
||||
printer errors. Those are errors that Marlin may report if there is an issue
|
||||
with the printer's SD card.
|
||||
* The "CuraEngine" plugin now makes it more obvious that it only targets
|
||||
CuraEngine versions up to and including 15.04 and also links to the plugin's
|
||||
homepage with more information right within the settings dialog.
|
||||
* Browser tab visibility is now tracked by the web interface, disabling the
|
||||
webcam and the GCODE viewer if the tab containing OctoPrint is not active.
|
||||
That should reduce the amount of resource utilized by the web interface on
|
||||
the client when it is not actively monitored. Might also help to mitigate
|
||||
[#1065](https://github.com/foosel/OctoPrint/issues/1065), the final verdict
|
||||
on that one is still out though.
|
||||
* The printer log in the terminal tab will now be cut off after 3000 lines
|
||||
even if autoscroll is disabled. If the limit is reached, no more log lines
|
||||
will be added to the client's buffer. That ensures that the log will not
|
||||
scroll and the current log excerpt will stay put while also not causing
|
||||
the browser to run into memory errors due to trying to buffer an endless
|
||||
amount of log lines.
|
||||
* Increased timeout of "waiting for restart" after an update from 20 to 60sec
|
||||
(20sec turned out to be too little for OctoPi for whatever reason).
|
||||
* Added a couple of unit tests
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#1120](https://github.com/foosel/OctoPrint/issues/1120) - Made the watchdog
|
||||
that monitors and handles the `watched` folder more resilient towards errors.
|
||||
* [#1125](https://github.com/foosel/OctoPrint/issues/1125) - Fixed OctoPrint
|
||||
displaying bed temperature and controls and allowing the sending of GCODE
|
||||
commands targeting the bed (`M140`, `M190`) if the printer profile doesn't
|
||||
have a heated bed configured.
|
||||
* Fixed an issue that stopped the software updater working for OctoPrint. The
|
||||
updater reports success updating, but no update has actually taken place. A
|
||||
fix can be applied for this issue to OctoPrint version 1.2.7 via
|
||||
[the Updatefix 1.2.7 plugin](https://github.com/OctoPrint/OctoPrint-Updatefix-1.2.7).
|
||||
For more information please refer to the [Important information for people updating from version 1.2.7](#important-information-for-people-updating-from-version-127)
|
||||
above.
|
||||
* Fix: Current filename in job data should never be prefixed with `/`
|
||||
* Only persist plugin settings that differ from the defaults. This way the
|
||||
`config.yaml` won't be filled with lots of redundant data. It's the
|
||||
responsibility of the plugin authors to responsibly handle changes in default
|
||||
settings of their plugins and add data migration where necessary.
|
||||
* Fixed a documentation bug ([#1067](https://github.com/foosel/OctoPrint/pull/1067))
|
||||
* Fixed a conflict with bootstrap-responsive, e.g. when using the
|
||||
[ScreenSquish Plugin](http://plugins.octoprint.org/plugins/screensquish/)
|
||||
([#1103](https://github.com/foosel/OctoPrint/pull/1067))
|
||||
* Fixed OctoPrint still sending SD card related commands to the printer even
|
||||
if SD card support is disabled (e.g. `M21`).
|
||||
* Hidden files are no longer visible to the template engine, neither as (GCODE)
|
||||
scripts nor as interface templates.
|
||||
* The hostname and URL prefix via which the OctoPrint web interface is accessed
|
||||
is now part of the cache key. Without that being the case the cache could
|
||||
be created referring to something like `/octoprint/prefix/api/` for its API
|
||||
endpoint (if accessed via `http://somehost:someport/octoprint/prefix/` first
|
||||
time), which would then cause the interface to not work if accessed later
|
||||
via another route (e.g. `http://someotherhost/`).
|
||||
* Fixed a JavaScript error on finishing streaming of a file to SD.
|
||||
* Fixed version reporting on detached HEADs (when the branch detection
|
||||
reported "HEAD" instead of "(detached"
|
||||
* Fixed some path checks for systems with symlinked paths
|
||||
([#1051](https://github.com/foosel/OctoPrint/pull/1051))
|
||||
* Fixed a bug causing the "Server Offline" overlay to pop _under_ the
|
||||
"Please reload" overlay, which could lead to "Connection refused" browser
|
||||
messages when clicking "Reload now" in the wrong moment.
|
||||
|
||||
([Commits](https://github.com/foosel/OctoPrint/compare/1.2.7...1.2.8))
|
||||
|
||||
## 1.2.7 (2015-10-20)
|
||||
|
||||
### Improvements
|
||||
|
|
@ -386,7 +509,7 @@
|
|||
changed under "Temperatures" in the Settings ([#343](https://github.com/foosel/OctoPrint/issues/343)).
|
||||
* High-DPI support for the GCode viewer ([#837](https://github.com/foosel/OctoPrint/issues/837)).
|
||||
* Stop websocket connections from multiplying ([#888](https://github.com/foosel/OctoPrint/pull/888)).
|
||||
* New setting to rotate webcam by 90° counter clockwise ([#895](https://github.com/foosel/OctoPrint/issues/895) and
|
||||
* New setting to rotate webcam by 90° counter clockwise ([#895](https://github.com/foosel/OctoPrint/issues/895) and
|
||||
[#906](https://github.com/foosel/OctoPrint/pull/906))
|
||||
* System commands now be set to a) run asynchronized by setting their `async` property to `true` and b) to ignore their
|
||||
result by setting their `ignore` property to `true`.
|
||||
|
|
|
|||
297
CONTRIBUTING.md
297
CONTRIBUTING.md
|
|
@ -1,51 +1,264 @@
|
|||
Issues, Tickets, however you may call them
|
||||
------------------------------------------
|
||||
# Contribution Guidelines
|
||||
|
||||
Read the following short instructions **fully** and **follow them** if you want your ticket to be taken care of and not closed again directly! They are linked on top of every new issue form, so don't say nobody warned you afterwards.
|
||||
This document outlines what you need to know before **[creating tickets](#issues-tickets-however-you-may-call-them)**
|
||||
or **[creating pull requests](#pull-requests)**.
|
||||
|
||||
- **Read the [FAQ](https://github.com/foosel/OctoPrint/wiki/FAQ)**
|
||||
- Always create **one ticket for one purpose**. So don't mix two or more feature requests, support requests, bugs etc into one ticket. If you do, your ticket will be closed!
|
||||
- If you want to report a bug, **READ AND FOLLOW [How to file a bug report](https://github.com/foosel/OctoPrint/wiki/How-to-file-a-bug-report)!** Tickets will be automatically checked if they comply with the requirements outlined in that wiki node! Other then what's written in there (**and really EVERYTHING that is written in there!**) you don't have to do anything special with your ticket. Listen to what GitIssueBot might have to say to you!
|
||||
- If you want to post a **request** of any kind (feature request, documentation request, ...), **add [Request] to your issue's title!**
|
||||
- If you need **support** with a problem of your installation (e.g. if you have problems getting the webcam to work) or have a general **question**, the issue tracker is not the right place. Consult the [Mailinglist](https://groups.google.com/group/octoprint) or the [Google+ Community](https://plus.google.com/communities/102771308349328485741) instead!
|
||||
- If you are a developer that wants to brainstorm a pull request or possible changes to the plugin system, **add [Brainstorming] to your issue's title!** (see below).
|
||||
- If you have another reason for creating a ticket that doesn't fit any of the above categories, it's something better suited for the [Mailinglist](https://groups.google.com/group/octoprint) or the [Google+ Community](https://plus.google.com/communities/102771308349328485741).
|
||||
## Contents
|
||||
|
||||
Following these guidelines (**especially EVERYTHING mentioned in ["How to file a bug report"](https://github.com/foosel/OctoPrint/wiki/How-to-file-a-bug-report)**) is necessary so the tickets stay manageable - you are not the only one with an open issue, so please respect that you have to **play by the rules** so that your problem can be taken care of. Tickets not playing by the rules **will be closed without further investigation!**.
|
||||
* [Issues, Tickets, however you may call them](#issues-tickets-however-you-may-call-them)
|
||||
* [How to file a bug report](#how-to-file-a-bug-report)
|
||||
* [What should I do before submitting a bug report?](#what-should-i-do-before-submitting-a-bug-report)
|
||||
* [What should I include in a bug report?](#what-should-i-include-in-a-bug-report)
|
||||
* [Where can I find which version and branch I'm on?](#where-can-i-find-which-version-and-branch-im-on)
|
||||
* [Where can I find those log files you keep talking about?](#where-can-i-find-those-log-files-you-keep-talking-about)
|
||||
* [Where can I find my browser's error console?](#where-can-i-find-my-browsers-error-console)
|
||||
* [Pull requests](#pull-requests)
|
||||
* [History](#history)
|
||||
* [Footnotes](#footnotes)
|
||||
|
||||
Pull Requests
|
||||
-------------
|
||||
## Issues, Tickets, however you may call them
|
||||
|
||||
1. If you want to add a new feature to OctoPrint, **please always first consider if it wouldn't be better suited for a
|
||||
plugin.** As a general rule of thumb, any feature that is only of interest to a small sub group should be moved into a
|
||||
plugin. If the current plugin system doesn't allow you to implement your feature as a plugin, create a "Brainstorming"
|
||||
ticket to get the discussion going how best to solve *this* in OctoPrint's plugin system - maybe that's the actual PR
|
||||
you have been waiting for to contribute :)
|
||||
2. If you plan to make **any large changes to the code or appearance, please open a "Brainstorming" ticket first** so that
|
||||
we can determine if it's a good time for your specific pull request. It might be that I'm currently in the process of
|
||||
making heavy changes to the code locations you'd target as well, or your approach doesn't fit the general "project
|
||||
vision", and that would just cause unnecessary work and frustration for everyone or possibly get the PR rejected.
|
||||
3. When adding code to OctoPrint, make sure you **follow the current coding style**. That means tabs instead of spaces in the
|
||||
python files (yes, I know that this goes against PEP-8, I don't care) and space instead of tabs in the Javascript sources,
|
||||
english language (that means code, variables, comments!), comments where necessary (tell why the code does something like
|
||||
it does it, structure your code), following the general architecture. If your PR needs to make changes to the Stylesheets,
|
||||
change the ``.less`` files from which the CSS is compiled. PRs that contain direct changes to the compiled
|
||||
CSS will be closed.
|
||||
4. **Test your changes thoroughly**. That also means testing with usage scenarios you don't normally use, e.g. if you only
|
||||
use access control, test without and vice versa. If you only test with your printer, test with the virtual printer and
|
||||
vice versa. State in your pull request how your tested your changes.
|
||||
5. Please create all pull requests **against the `devel` branch**.
|
||||
6. Create **one pull request per feature/bug fix**.
|
||||
7. Create a **custom branch** for your feature/bug fix and use that as base for your pull request. Pull requests directly
|
||||
against your version of `devel` will be closed.
|
||||
8. In your pull request's description, **state what your pull request is doing**, as in, what feature does it implement, what
|
||||
bug does it fix. The more thoroughly you explain your intent behind the PR here, the higher the chances it will get merged
|
||||
fast.
|
||||
9. Don't forget to **add yourself to the [AUTHORS](../AUTHORS.md) file** :)
|
||||
Please read the following short instructions fully and follow them. You can
|
||||
help the project tremendously this way: not only do you help the maintainers
|
||||
to **address problems in a timely manner** but also keep it possible for them
|
||||
to **fix bugs, add new and improve on existing functionality** instead of doing
|
||||
nothing but ticket management.
|
||||
|
||||
History
|
||||
-------
|
||||

|
||||
|
||||
* 2015-01-23: More guidelines for creating pull requests, support/questions redirected to Mailinglist/G+ community
|
||||
- **[Read the FAQ](https://github.com/foosel/OctoPrint/wiki/FAQ)**
|
||||
- If you want to report a **bug**, [read "How to file a bug report" below](#how-to-file-a-bug-report)
|
||||
and *[use the provided template](#what-should-i-include-in-a-ticket)*.
|
||||
You do not need to do anything else with your ticket.
|
||||
- If you want to post a **request** of any kind (feature request, documentation
|
||||
request, ...), add `[Request]` to your issue's title (e.g. `[Request] Awesome new feature`).
|
||||
- If you are a **developer** that wants to brainstorm a pull request or possible
|
||||
changes to the plugin system, add [Brainstorming] to your issue's title (e.g.
|
||||
`[Brainstorming] New plugin hook for doing some cool stuff`).
|
||||
- If you need **support**, have a **question** or some **other reason** that
|
||||
doesn't fit any of the above categories, the issue tracker is not the right place.
|
||||
Consult the [Mailinglist](https://groups.google.com/group/octoprint) or the
|
||||
[Google+ Community](https://plus.google.com/communities/102771308349328485741) instead.
|
||||
|
||||
No matter what kind of ticket you create, never mix two or more "ticket reasons"
|
||||
into one ticket: One ticket per bug, request, brainstorming thread please.
|
||||
|
||||
----
|
||||
|
||||
**Note**: A bot is in place that monitors new tickets, automatically
|
||||
categorizes them and checks new bug reports for usage of the provided template.
|
||||
That bot will only bother you if you open a ticket that appears to be a bug (no
|
||||
`[Request]` or `[Brainstorming]` in the title) without the template, and it
|
||||
will do that only to ensure that all information needed to solve the issue is
|
||||
available for the maintainers to directly start tackling that problem.
|
||||
|
||||
----
|
||||
|
||||
## How to file a bug report
|
||||
|
||||
If you encounter an issue with OctoPrint, you are welcome to
|
||||
[submit a bug report](https://goo.gl/GzkGv9).
|
||||
|
||||
Before you do that for the first time though please take a moment to read the
|
||||
following section *completely*. Thank you! :)
|
||||
|
||||
### What should I do before submitting a bug report?
|
||||
|
||||
1. **Make sure you are at the right location**. This is the Github repository
|
||||
of the official version of OctoPrint, which is the 3D print server and
|
||||
corresponding web interface itself.
|
||||
|
||||
**This is not the Github respository of OctoPi**, which is the preconfigured
|
||||
Raspberry Pi image including OctoPrint among other things - that one can be found
|
||||
[here](https://github.com/guysoft/OctoPi). Please note that while we do have
|
||||
some entries regarding OctoPi in the FAQ, any bugs should be reported in the
|
||||
[proper bug tracker](https://github.com/guysoft/OctoPi/issues) which - again -
|
||||
is not here.
|
||||
|
||||
**This is also not the Github repository of any OctoPrint Plugins you
|
||||
might have installed**. Report any issues with those in their corresponding
|
||||
bug tracker (probably linked to from the plugin's homepage).
|
||||
|
||||
Finally, **this is also not the right issue tracker if you are running an
|
||||
forked version of OctoPrint**. Seek help for such unofficial versions from
|
||||
the people maintaining them instead.
|
||||
|
||||
2. Please make sure to **test out the current version** of OctoPrint to see
|
||||
whether the problem you are encountering still exists.
|
||||
|
||||
If you are feeling up to it you might also want to try the current development
|
||||
version of OctoPrint (if you aren't already). Refer to the [FAQ](https://github.com/foosel/OctoPrint/wiki/FAQ)
|
||||
for information on how to do this.
|
||||
|
||||
3. The problem still exists? Then please **look through the
|
||||
[existing tickets](https://github.com/foosel/OctoPrint/issues?state=open)
|
||||
(use the [search](https://github.com/foosel/OctoPrint/search?q=&ref=cmdform&type=Issues))**
|
||||
to check if there already exists a report of the issue you are encountering.
|
||||
Sorting through duplicates of the same issue sometimes causes more work than
|
||||
fixing it. Take the time to filter through possible duplicates and be really
|
||||
sure that your problem definitely is a new one. Try more than one search query
|
||||
(e.g. do not only search for "webcam" if you happen to run into an issue
|
||||
with your webcam, also search for "timelapse" etc).
|
||||
|
||||
### What should I include in a bug report?
|
||||
|
||||
Always use the following template (you can remove what's within `[...]`, that's
|
||||
only provided here as some additional information for you):
|
||||
|
||||
#### What were you doing?
|
||||
|
||||
[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. 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?
|
||||
|
||||
#### What happened instead?
|
||||
|
||||
#### Branch & Commit or Version of OctoPrint
|
||||
|
||||
[Can be found in the lower left corner of the web interface.]
|
||||
|
||||
#### Printer model & used firmware incl. version
|
||||
|
||||
[If applicable, always include if unsure.]
|
||||
|
||||
#### Browser and Version of Browser, Operating System running Browser
|
||||
|
||||
[If applicable, always include if unsure.]
|
||||
|
||||
#### Link to octoprint.log
|
||||
|
||||
[On gist.github.com or pastebin.com. Always include and never truncate.]
|
||||
|
||||
#### Link to contents of terminal tab or serial.log
|
||||
|
||||
[On gist.github.com or pastebin.com. If applicable, always include if unsure or
|
||||
reporting communication issues. Never truncate.]
|
||||
|
||||
#### 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.]
|
||||
|
||||
#### Screenshot(s) showing the problem:
|
||||
|
||||
[If applicable. Always include if unsure or reporting UI issues.]
|
||||
|
||||
I have read the FAQ.
|
||||
|
||||
### Where can I find which version and branch I'm on?
|
||||
|
||||
You can find out all of them by taking a look into the lower left corner of the
|
||||
OctoPrint UI:
|
||||
|
||||

|
||||
|
||||
If you don't have access to the UI you can find out that information via the
|
||||
command line as well. Either `octoprint --version` or `python setup.py version`
|
||||
in OctoPrint's folder will tell you the version of OctoPrint you are running
|
||||
(note: if it doesn't then you are running a version older than 1.1.0,
|
||||
*upgrade now*). A `git branch` in your OctoPrint installation folder will mark
|
||||
the branch you are on with a little *. `git rev-parse HEAD` will tell you the
|
||||
current commit.
|
||||
|
||||
### Where can I find those log files you keep talking about?
|
||||
|
||||
OctoPrint by default provides two log outputs, a third one can be enabled if
|
||||
more information is needed.
|
||||
|
||||
One is contained in the **"Terminal" tab** within OctoPrint's UI and is a log
|
||||
of the last 300 lines of communication with the printer. Please copy-paste
|
||||
this somewhere (disable auto scroll to make copying the contents easier) -
|
||||
e.g. http://pastebin.com or http://gist.github.com - and include a link in
|
||||
your bug report.
|
||||
|
||||
There is also **OctoPrint's application log file** or in short `octoprint.log`,
|
||||
which is by default located at `~/.octoprint/logs/octoprint.log` on Linux,
|
||||
`%APPDATA%\OctoPrint\logs\octoprint.log` on Windows and
|
||||
`~/Library/Application Support/OctoPrint/logs/octoprint.log` on MacOS. Please
|
||||
copy-paste this to pastebin or gist as well and include a link in your bug
|
||||
report.
|
||||
|
||||
It might happen that you are asked to provide a more **thorough log of the
|
||||
communication with the printer** if you haven't already done so, the `serial.log`.
|
||||
This is not written by default due to performance reasons, but you can enable
|
||||
it in the settings dialog. After enabling that log, please reproduce the problem
|
||||
again (connect to the printer, do whatever triggers it), then copy-paste
|
||||
`~/.octoprint/logs/serial.log` (Windows: `%APPDATA%\OctoPrint\logs\serial.log`,
|
||||
MacOS: `~/Library/Application Support/OctoPrint/logs/serial.log`) to pastebin
|
||||
or gist and include the link in the bug report.
|
||||
|
||||
You might also be asked to provide a log with an increased log level. You can
|
||||
find information on how to do just that in the
|
||||
[docs](http://docs.octoprint.org/en/master/configuration/logging_yaml.html).
|
||||
|
||||
### Where can I find my browser's error console?
|
||||
|
||||
See [How to open the Javascript Console in different browsers](https://webmasters.stackexchange.com/questions/8525/how-to-open-the-javascript-console-in-different-browsers)
|
||||
|
||||
## Pull requests
|
||||
|
||||
1. If you want to add a new feature to OctoPrint, **please always first
|
||||
consider if it wouldn't be better suited for a plugin.** As a general rule
|
||||
of thumb, any feature that is only of interest to a small sub group should
|
||||
be moved into a plugin. If the current plugin system doesn't allow you to
|
||||
implement your feature as a plugin, create a "Brainstorming" ticket to get
|
||||
the discussion going on how best to solve *this* in OctoPrint's plugin
|
||||
system - maybe that's the actual PR you have been waiting for to contribute :)
|
||||
2. If you plan to make **any large changes to the code or appearance, please
|
||||
open a "Brainstorming" ticket first** so that we can determine if it's a
|
||||
good time for your specific pull request. It might be that we're currently
|
||||
in the process of making heavy changes to the code locations you'd target
|
||||
as well, or your approach doesn't fit the general "project vision", and
|
||||
that would just cause unnecessary work and frustration for everyone or
|
||||
possibly get the PR rejected.
|
||||
3. Create your pull request **from a custom branch** on your end (e.g.
|
||||
`dev/myNewFeature`)[1] **against the `devel` branch**. Create **one pull request
|
||||
per feature/bug fix**. If your PR contains an important bug fix, we will
|
||||
make sure to backport it to the `maintenance` branch to also include it in
|
||||
the next release.
|
||||
4. Make sure you **follow the current coding style**. This means:
|
||||
* Tabs instead of spaces in the Python files[2]
|
||||
* Spaces instead of tabs in the Javascript sources
|
||||
* English language (code, variables, comments, ...)
|
||||
* Comments where necessary: Tell why the code does something like it does
|
||||
it, structure your code
|
||||
* Following the general architecture
|
||||
If your PR needs to make changes to the Stylesheets, change the ``.less`` files
|
||||
from which the CSS is compiled.
|
||||
5. **Test your changes thoroughly**. That also means testing with usage
|
||||
scenarios you don't normally use, e.g. if you only use access control, test
|
||||
without and vice versa. If you only test with your printer, test with the
|
||||
virtual printer and vice versa. State in your pull request how your tested
|
||||
your changes. Ideally **add unit tests** - OctoPrint severly lacks in that
|
||||
department, but we are trying to change that, so any new code already covered
|
||||
with a test suite helps a lot!
|
||||
6. In your pull request's description, **state what your pull request is doing**,
|
||||
as in, what feature does it implement, what bug does it fix. The more
|
||||
thoroughly you explain your intent behind the PR here, the higher the
|
||||
chances it will get merged fast.
|
||||
7. Important: Don't forget to **add yourself to the [AUTHORS](./AUTHORS.md)
|
||||
file** :)
|
||||
|
||||
## History
|
||||
|
||||
* 2015-01-23: More guidelines for creating pull requests, support/questions
|
||||
redirected to Mailinglist/G+ community
|
||||
* 2015-01-27: Added another explicit link to the FAQ
|
||||
* 2015-07-07: Added step to add yourself to AUTHORS when creating a PR :)
|
||||
* 2015-12-01: Heavily reworked to include examples, better structure and
|
||||
all information in one document.
|
||||
|
||||
## Footnotes
|
||||
* [1] - If you are wondering why, the problem is that anything that you add
|
||||
to your PR's branch will also become part of your PR, so if you create a
|
||||
PR from your version of `devel` chances are high you'll add changes to the
|
||||
PR that do not belong to the PR.
|
||||
* [2] - Yes, we know that this goes against PEP-8. OctoPrint started out as
|
||||
a fork of Cura and hence stuck to the coding style found therein. Changing
|
||||
it now would make the history and especially `git blame` completely
|
||||
unusable, so for now we'll have to deal with it (this decision might be
|
||||
revisited in the future).
|
||||
|
|
|
|||
|
|
@ -47,11 +47,11 @@ which is a custom SD card image that includes OctoPrint plus dependencies.
|
|||
The generic steps that should basically be done regardless of operating system
|
||||
and runtime environment are the following (as *regular
|
||||
user*, please keep your hands *off* of the `sudo` command here!) - this assumes
|
||||
you already have Python 2.7, pip and virtualenv set up:
|
||||
you already have Python 2.7, pip and virtualenv set up on your system:
|
||||
|
||||
1. Checkout OctoPrint: `git clone https://github.com/foosel/OctoPrint.git`
|
||||
2. Change into the OctoPrint folder: `cd OctoPrint`
|
||||
3. Create a user-owned virtual environment therein: `virtualenv --system-site-packages venv`
|
||||
3. Create a user-owned virtual environment therein: `virtualenv venv`
|
||||
4. Install OctoPrint *into that virtual environment*: `./venv/bin/python setup.py install`
|
||||
|
||||
You may then start the OctoPrint server via `/path/to/OctoPrint/venv/bin/octoprint`, see [Usage](#usage)
|
||||
|
|
|
|||
|
|
@ -25,7 +25,8 @@ Tool
|
|||
See :ref:`sec-api-printer-toolcommand`.
|
||||
Bed
|
||||
Bed commands allow setting the temperature and temperature offset for the printer's heated bed. Querying the
|
||||
corresponding resource returns temperature information including an optional history.
|
||||
corresponding resource returns temperature information including an optional history. Note that Bed commands
|
||||
are only available if the currently selected printer profile has a heated bed.
|
||||
See :ref:`sec-api-printer-bedcommand`.
|
||||
SD card
|
||||
SD commands allow initialization, refresh and release of the printer's SD card (if available). Querying the
|
||||
|
|
@ -564,6 +565,9 @@ Issue a bed command
|
|||
|
||||
Upon success, a status code of :http:statuscode:`204` and an empty body is returned.
|
||||
|
||||
If no heated bed is configured for the currently selected printer profile, the resource will return
|
||||
an :http:statuscode:`409`.
|
||||
|
||||
**Example Target Temperature Request**
|
||||
|
||||
Set the target temperature for the printer's heated bed to 75°C.
|
||||
|
|
@ -610,7 +614,8 @@ Issue a bed command
|
|||
:statuscode 204: No error
|
||||
:statuscode 400: If ``target`` or ``offset`` is not a valid number or outside of the supported range, or if the
|
||||
request is otherwise invalid.
|
||||
:statuscode 409: If the printer is not operational.
|
||||
:statuscode 409: If the printer is not operational or the selected printer profile
|
||||
does not have a heated bed.
|
||||
|
||||
.. _sec-api-printer-bedstate:
|
||||
|
||||
|
|
@ -627,6 +632,9 @@ Retrieve the current bed state
|
|||
|
||||
Returns a :http:statuscode:`200` with a Temperature Response in the body upon success.
|
||||
|
||||
If no heated bed is configured for the currently selected printer profile, the resource will return
|
||||
an :http:statuscode:`409`.
|
||||
|
||||
.. note::
|
||||
If you want both tool and bed temperature information at the same time, take a look at
|
||||
:ref:`Retrieve the current printer state <sec-api-printer-state>`.
|
||||
|
|
@ -675,7 +683,8 @@ Retrieve the current bed state
|
|||
:query limit: If set to an integer (``n``), only the last ``n`` data points from the printer's temperature history
|
||||
will be returned. Will be ignored if ``history`` is not enabled.
|
||||
:statuscode 200: No error
|
||||
:statuscode 409: If the printer is not operational.
|
||||
:statuscode 409: If the printer is not operational or the selected printer profile
|
||||
does not have a heated bed.
|
||||
|
||||
.. _sec-api-printer-sdcommand:
|
||||
|
||||
|
|
@ -856,7 +865,7 @@ Send an arbitrary command to the printer
|
|||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 204 No Content
|
||||
|
||||
|
||||
:json string command: Single command to send to the printer, mutually exclusive with ``commands``.
|
||||
:json string commands: List of commands to send to the printer, mutually exclusive with ``command``.
|
||||
:statuscode 204: No error
|
||||
|
|
@ -913,7 +922,8 @@ Temperature State
|
|||
* - ``bed``
|
||||
- 0..1
|
||||
- :ref:`Temperature Data <sec-api-datamodel-printer-tempdata>`
|
||||
- Current temperature stats for the printer's heated bed. Not included if querying only tool state.
|
||||
- Current temperature stats for the printer's heated bed. Not included if querying only tool state or if
|
||||
the currently selected printer profile does not have a heated bed.
|
||||
* - ``history``
|
||||
- 0..1
|
||||
- List of :ref:`Historic Temperature Datapoint <sec-api-datamodel-printer-temphistory>`
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ a GCODE script including user input.
|
|||
children:
|
||||
- name: Get Position
|
||||
command: M114
|
||||
regex: "X:([0-9.]+) Y:([0-9.]+) Z:([0-9.]+) E:([0-9.]+)"
|
||||
regex: "X:([-+]?[0-9.]+) Y:([-+]?[0-9.]+) Z:([-+]?[0-9.]+) E:([-+]?[0-9.]+)"
|
||||
template: "Position: X={0}, Y={1}, Z={2}, E={3}"
|
||||
- name: Fun stuff
|
||||
children:
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ Out of the box, OctoPrint defaults to the following script setup for ``afterPrin
|
|||
|
||||
;disable all heaters
|
||||
{% snippet 'disable_hotends' %}
|
||||
M140 S0
|
||||
[% snippet 'disable_bed' %}
|
||||
|
||||
;disable fan
|
||||
M106 S0
|
||||
|
|
@ -100,8 +100,19 @@ The ``disable_hotends`` snippet is defined as follows:
|
|||
M104 T{{ tool }} S0
|
||||
{% endfor %}
|
||||
|
||||
As you can see, the ``disable_hotends`` snippet utilizes the ``printer_profile`` context variable in order to
|
||||
iterate through all available extruders and set their temperature to 0.
|
||||
The ``disable_bed`` snippet is defined as follows:
|
||||
|
||||
.. code-block:: jinja
|
||||
:caption: Default ``disable_bed`` snippet
|
||||
|
||||
{% if printer_profile.heatedBed %}
|
||||
M140 S0
|
||||
{% endif %}
|
||||
|
||||
As you can see, the ``disable_hotends`` and ``disable_bed`` snippets utilize the
|
||||
``printer_profile`` context variable in order to iterate through all available
|
||||
extruders and set their temperature to 0, and to also set the bed temperature
|
||||
to 0 if a heated bed is configured.
|
||||
|
||||
.. seealso::
|
||||
|
||||
|
|
|
|||
3
setup.py
3
setup.py
|
|
@ -32,7 +32,8 @@ INSTALL_REQUIRES = [
|
|||
"pkginfo==1.2.1",
|
||||
"requests==2.7.0",
|
||||
"semantic_version==2.4.2",
|
||||
"psutil==3.2.1"
|
||||
"psutil==3.2.1",
|
||||
"awesome-slugify>=1.6.5,<1.7"
|
||||
]
|
||||
|
||||
# Additional requirements for optional install options
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ import tempfile
|
|||
|
||||
import octoprint.filemanager
|
||||
|
||||
from octoprint.util import is_hidden_path
|
||||
|
||||
class StorageInterface(object):
|
||||
"""
|
||||
Interface of storage adapters for OctoPrint.
|
||||
|
|
@ -309,6 +311,10 @@ class LocalFileStorage(StorageInterface):
|
|||
|
||||
self._metadata_cache = pylru.lrucache(10)
|
||||
|
||||
from slugify import Slugify
|
||||
self._slugify = Slugify()
|
||||
self._slugify.safe_chars = "-_.() "
|
||||
|
||||
self._old_metadata = None
|
||||
self._initialize_metadata()
|
||||
|
||||
|
|
@ -357,7 +363,7 @@ class LocalFileStorage(StorageInterface):
|
|||
if not metadata:
|
||||
metadata = dict()
|
||||
for entry in os.listdir(path):
|
||||
if entry.startswith(".") or not octoprint.filemanager.valid_file_type(entry):
|
||||
if is_hidden_path(entry) or not octoprint.filemanager.valid_file_type(entry):
|
||||
continue
|
||||
|
||||
absolute_path = os.path.join(path, entry)
|
||||
|
|
@ -607,9 +613,9 @@ class LocalFileStorage(StorageInterface):
|
|||
|
||||
def sanitize_name(self, name):
|
||||
"""
|
||||
Raises a :class:`ValueError` for a ``name`` containing ``/`` or ``\``. Otherwise strips any characters from the
|
||||
given ``name`` that are not any of the ASCII characters, digits, ``-``, ``_``, ``.``, ``(``, ``)`` or space and
|
||||
replaces and spaces with ``_``.
|
||||
Raises a :class:`ValueError` for a ``name`` containing ``/`` or ``\``. Otherwise
|
||||
slugifies the given ``name`` by converting it to ASCII, leaving ``-``, ``_``, ``.``,
|
||||
``(``, and ``)`` as is.
|
||||
"""
|
||||
if name is None:
|
||||
return None
|
||||
|
|
@ -617,11 +623,7 @@ class LocalFileStorage(StorageInterface):
|
|||
if "/" in name or "\\" in name:
|
||||
raise ValueError("name must not contain / or \\")
|
||||
|
||||
import string
|
||||
valid_chars = "-_.() {ascii}{digits}".format(ascii=string.ascii_letters, digits=string.digits)
|
||||
sanitized_name = ''.join(c for c in name if c in valid_chars)
|
||||
sanitized_name = sanitized_name.replace(" ", "_")
|
||||
return sanitized_name
|
||||
return self._slugify(name).replace(" ", "_")
|
||||
|
||||
def sanitize_path(self, path):
|
||||
"""
|
||||
|
|
@ -887,7 +889,7 @@ class LocalFileStorage(StorageInterface):
|
|||
|
||||
result = dict()
|
||||
for entry in os.listdir(path):
|
||||
if entry.startswith("."):
|
||||
if is_hidden_path(entry):
|
||||
# no hidden files and folders
|
||||
continue
|
||||
|
||||
|
|
|
|||
|
|
@ -330,7 +330,7 @@ class PluginSettings(object):
|
|||
set_int =("setInt", prefix_path_in_args, add_setter_kwargs),
|
||||
set_float =("setFloat", prefix_path_in_args, add_setter_kwargs),
|
||||
set_boolean=("setBoolean", prefix_path_in_args, add_setter_kwargs),
|
||||
remove =("remove", prefix_path_in_args)
|
||||
remove =("remove", prefix_path_in_args, lambda x: x)
|
||||
)
|
||||
self.deprecated_access_methods = dict(
|
||||
getInt ="get_int",
|
||||
|
|
|
|||
|
|
@ -37,11 +37,10 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin,
|
|||
|
||||
##~~ TemplatePlugin API
|
||||
|
||||
def get_template_configs(self):
|
||||
from flask.ext.babel import gettext
|
||||
return [
|
||||
dict(type="settings", name=gettext("CuraEngine"))
|
||||
]
|
||||
def get_template_vars(self):
|
||||
return dict(
|
||||
homepage=__plugin_url__
|
||||
)
|
||||
|
||||
##~~ StartupPlugin API
|
||||
|
||||
|
|
@ -413,9 +412,9 @@ def _sanitize_name(name):
|
|||
sanitized_name = sanitized_name.replace(" ", "_")
|
||||
return sanitized_name.lower()
|
||||
|
||||
__plugin_name__ = "CuraEngine"
|
||||
__plugin_name__ = "CuraEngine (<= 15.04)"
|
||||
__plugin_author__ = "Gina Häußge"
|
||||
__plugin_url__ = "https://github.com/foosel/OctoPrint/wiki/Plugin:-Cura"
|
||||
__plugin_description__ = "Adds support for slicing via CuraEngine from within OctoPrint"
|
||||
__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()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
<h4>{{ _('General') }}</h4>
|
||||
|
||||
<p>{% trans %}
|
||||
Specify the path to the CuraEngine binary. Note that only
|
||||
<strong>versions up to and including 15.04</strong> are supported.
|
||||
CuraEngine version 15.06 or newer is <strong>not</strong>
|
||||
compatible with this plugin.
|
||||
{% endtrans %}</p>
|
||||
|
||||
<form class="form-horizontal">
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="settings-cura-path">{{ _('Path to CuraEngine') }}</label>
|
||||
|
|
@ -10,7 +17,7 @@
|
|||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" data-bind="checked: settings.plugins.cura.debug_logging"> {{ _('Log the output of CuraEngine to plugin_cura_engine.log') }}
|
||||
<input type="checkbox" data-bind="checked: settings.plugins.cura.debug_logging"> {{ _('Log the output of CuraEngine to <code>plugin_cura_engine.log</code>') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -53,6 +60,10 @@
|
|||
|
||||
<button class="btn pull-right" data-bind="click: function() { $root.showImportProfileDialog() }">{{ _('Import Profile...') }}</button>
|
||||
|
||||
<div style="clear: both">
|
||||
<small>{% trans %}For more information on configuration and usage please <a href="{{ plugin_cura_homepage }}" target="_blank">see the Plugin's homepage</a>.{% endtrans %}</small>
|
||||
</div>
|
||||
|
||||
<div id="settings_plugin_cura_import" class="modal hide fade">
|
||||
<div class="modal-header">
|
||||
<a href="#" class="close" data-dismiss="modal" aria-hidden="true">×</a>
|
||||
|
|
@ -98,6 +109,13 @@
|
|||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<small>{% trans %}
|
||||
You can import your existing profile <code>.ini</code> files from Cura (version up to and
|
||||
including 15.04) here. Please be aware that neither the <code>.json</code> profile format
|
||||
from Cura versions starting with 15.06 is supported, nor are the custom Cura profile formats
|
||||
that third party tools like e.g. Repetier create.
|
||||
{% endtrans %}</small>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn" data-dismiss="modal" aria-hidden="true">{{ _('Abort') }}</button>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import octoprint.plugin.core
|
|||
|
||||
from octoprint.settings import valid_boolean_trues
|
||||
from octoprint.server.util.flask import restricted_access
|
||||
from octoprint.server import admin_permission
|
||||
from octoprint.server import admin_permission, VERSION
|
||||
from octoprint.util.pip import PipCaller, UnknownPip
|
||||
|
||||
from flask import jsonify, make_response
|
||||
|
|
@ -184,7 +184,7 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
|
|||
plugins=self._repository_plugins
|
||||
),
|
||||
os=self._get_os(),
|
||||
octoprint=self._get_octoprint_version(),
|
||||
octoprint=VERSION,
|
||||
pip=dict(
|
||||
available=self._pip_caller.available,
|
||||
command=self._pip_caller.command,
|
||||
|
|
@ -550,9 +550,7 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
|
|||
return False
|
||||
|
||||
current_os = self._get_os()
|
||||
octoprint_version = self._get_octoprint_version()
|
||||
if "-" in octoprint_version:
|
||||
octoprint_version = octoprint_version[:octoprint_version.find("-")]
|
||||
octoprint_version = self._get_octoprint_version(base=True)
|
||||
|
||||
def map_repository_entry(entry):
|
||||
result = dict(entry)
|
||||
|
|
@ -577,12 +575,11 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
|
|||
self._repository_plugins = map(map_repository_entry, repo_data)
|
||||
return True
|
||||
|
||||
def _is_octoprint_compatible(self, octoprint_version_string, compatibility_entries):
|
||||
def _is_octoprint_compatible(self, octoprint_version, compatibility_entries):
|
||||
"""
|
||||
Tests if the current ``octoprint_version`` is compatible to any of the provided ``compatibility_entries``.
|
||||
"""
|
||||
|
||||
octoprint_version = pkg_resources.parse_version(octoprint_version_string)
|
||||
for octo_compat in compatibility_entries:
|
||||
if not any(octo_compat.startswith(c) for c in ("<", "<=", "!=", "==", ">=", ">", "~=", "===")):
|
||||
octo_compat = ">={}".format(octo_compat)
|
||||
|
|
@ -611,9 +608,30 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
|
|||
else:
|
||||
return "unknown"
|
||||
|
||||
def _get_octoprint_version(self):
|
||||
from octoprint._version import get_versions
|
||||
return get_versions()["version"]
|
||||
def _get_octoprint_version_string(self):
|
||||
return VERSION
|
||||
|
||||
def _get_octoprint_version(self, base=False):
|
||||
octoprint_version_string = self._get_octoprint_version_string()
|
||||
|
||||
if "-" in octoprint_version_string:
|
||||
octoprint_version_string = octoprint_version_string[:octoprint_version_string.find("-")]
|
||||
|
||||
octoprint_version = pkg_resources.parse_version(octoprint_version_string)
|
||||
if base:
|
||||
if isinstance(octoprint_version, tuple):
|
||||
# old setuptools
|
||||
base_version = []
|
||||
for part in octoprint_version:
|
||||
if part.startswith("*"):
|
||||
break
|
||||
base_version.append(part)
|
||||
base_version.append("*final")
|
||||
octoprint_version = tuple(base_version)
|
||||
else:
|
||||
# new setuptools
|
||||
octoprint_version = pkg_resources.parse_version(octoprint_version.base_version)
|
||||
return octoprint_version
|
||||
|
||||
def _to_external_representation(self, plugin):
|
||||
return dict(
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ from . import version_checks, updaters, exceptions, util
|
|||
|
||||
|
||||
from octoprint.server.util.flask import restricted_access
|
||||
from octoprint.server import admin_permission
|
||||
from octoprint.server import admin_permission, VERSION, REVISION
|
||||
from octoprint.util import dict_merge
|
||||
import octoprint.settings
|
||||
|
||||
|
|
@ -430,8 +430,6 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
|
|||
update_available = update_available or target_update_available
|
||||
update_possible = update_possible or (target_update_possible and target_update_available)
|
||||
|
||||
from octoprint._version import get_versions
|
||||
octoprint_version = get_versions()["version"]
|
||||
local_name = target_information["local"]["name"]
|
||||
local_value = target_information["local"]["value"]
|
||||
|
||||
|
|
@ -439,7 +437,7 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
|
|||
updatePossible=target_update_possible,
|
||||
information=target_information,
|
||||
displayName=populated_check["displayName"],
|
||||
displayVersion=populated_check["displayVersion"].format(octoprint_version=octoprint_version, local_name=local_name, local_value=local_value),
|
||||
displayVersion=populated_check["displayVersion"].format(octoprint_version=VERSION, local_name=local_name, local_value=local_value),
|
||||
check=populated_check)
|
||||
|
||||
if self._version_cache_dirty:
|
||||
|
|
@ -503,19 +501,28 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
|
|||
"""
|
||||
|
||||
checks = self._get_configured_checks()
|
||||
populated_checks = dict()
|
||||
for target, check in checks.items():
|
||||
try:
|
||||
populated_checks[target] = self._populated_check(target, check)
|
||||
except exceptions.UnknownCheckType:
|
||||
self._logger.debug("Ignoring unknown check type for target {}".format(target))
|
||||
except:
|
||||
self._logger.exception("Error while populating check prior to update for target {}".format(target))
|
||||
|
||||
if check_targets is None:
|
||||
check_targets = checks.keys()
|
||||
to_be_updated = sorted(set(check_targets) & set(checks.keys()))
|
||||
check_targets = populated_checks.keys()
|
||||
to_be_updated = sorted(set(check_targets) & set(populated_checks.keys()))
|
||||
if "octoprint" in to_be_updated:
|
||||
to_be_updated.remove("octoprint")
|
||||
tmp = ["octoprint"] + to_be_updated
|
||||
to_be_updated = tmp
|
||||
|
||||
updater_thread = threading.Thread(target=self._update_worker, args=(checks, to_be_updated, force))
|
||||
updater_thread = threading.Thread(target=self._update_worker, args=(populated_checks, to_be_updated, force))
|
||||
updater_thread.daemon = False
|
||||
updater_thread.start()
|
||||
|
||||
return to_be_updated, dict((key, check["displayName"] if "displayName" in check else key) for key, check in checks.items() if key in to_be_updated)
|
||||
return to_be_updated, dict((key, check["displayName"] if "displayName" in check else key) for key, check in populated_checks.items() if key in to_be_updated)
|
||||
|
||||
def _update_worker(self, checks, check_targets, force):
|
||||
|
||||
|
|
@ -680,12 +687,10 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
|
|||
result["displayName"] = check.get("displayName", gettext("OctoPrint"))
|
||||
result["displayVersion"] = check.get("displayVersion", "{octoprint_version}")
|
||||
|
||||
from octoprint._version import get_versions
|
||||
versions = get_versions()
|
||||
if check["type"] == "github_commit":
|
||||
result["current"] = versions.get("full-revisionid", versions.get("full", "unknown"))
|
||||
result["current"] = REVISION if REVISION else "unknown"
|
||||
else:
|
||||
result["current"] = versions["version"]
|
||||
result["current"] = VERSION
|
||||
else:
|
||||
result["displayName"] = check.get("displayName", target)
|
||||
result["displayVersion"] = check.get("displayVersion", check.get("current", "unknown"))
|
||||
|
|
|
|||
|
|
@ -387,7 +387,7 @@ $(function() {
|
|||
}
|
||||
});
|
||||
self.waitingForRestart = false;
|
||||
}, 20000);
|
||||
}, 60000);
|
||||
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,6 +70,8 @@ class VirtualPrinter():
|
|||
self._sendWait = settings().getBoolean(["devel", "virtualPrinter", "sendWait"])
|
||||
self._waitInterval = settings().getFloat(["devel", "virtualPrinter", "waitInterval"])
|
||||
|
||||
self._echoOnM117 = settings().getBoolean(["devel", "virtualPrinter", "echoOnM117"])
|
||||
|
||||
self.currentLine = 0
|
||||
self.lastN = 0
|
||||
|
||||
|
|
@ -234,7 +236,8 @@ class VirtualPrinter():
|
|||
continue
|
||||
elif "M117" in data:
|
||||
# we'll just use this to echo a message, to allow playing around with pause triggers
|
||||
self.outgoing.put("echo:%s" % re.search("M117\s+(.*)", data).group(1))
|
||||
if self._echoOnM117:
|
||||
self.outgoing.put("echo:%s" % re.search("M117\s+(.*)", data).group(1))
|
||||
elif "M999" in data:
|
||||
# mirror Marlin behaviour
|
||||
self.outgoing.put("Resend: 1")
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import re
|
|||
import logging
|
||||
|
||||
from octoprint.settings import settings
|
||||
from octoprint.util import dict_merge, dict_sanitize, dict_contains_keys
|
||||
from octoprint.util import dict_merge, dict_sanitize, dict_contains_keys, is_hidden_path
|
||||
|
||||
class SaveError(Exception):
|
||||
pass
|
||||
|
|
@ -151,7 +151,7 @@ class PrinterProfileManager(object):
|
|||
formFactor = BedTypes.RECTANGULAR,
|
||||
origin = BedOrigin.LOWERLEFT
|
||||
),
|
||||
heatedBed = False,
|
||||
heatedBed = True,
|
||||
extruder=dict(
|
||||
count = 1,
|
||||
offsets = [
|
||||
|
|
@ -289,7 +289,7 @@ class PrinterProfileManager(object):
|
|||
def _load_all_identifiers(self):
|
||||
results = dict(_default=None)
|
||||
for entry in os.listdir(self._folder):
|
||||
if entry.startswith(".") or not entry.endswith(".profile") or entry == "_default.profile":
|
||||
if is_hidden_path(entry) or not entry.endswith(".profile") or entry == "_default.profile":
|
||||
continue
|
||||
|
||||
path = os.path.join(self._folder, entry)
|
||||
|
|
|
|||
|
|
@ -22,9 +22,11 @@ import logging
|
|||
import logging.config
|
||||
import atexit
|
||||
import signal
|
||||
import base64
|
||||
|
||||
SUCCESS = {}
|
||||
NO_CONTENT = ("", 204)
|
||||
NOT_MODIFIED = ("Not Modified", 304)
|
||||
|
||||
app = Flask("octoprint")
|
||||
assets = None
|
||||
|
|
@ -42,6 +44,7 @@ loginManager = None
|
|||
pluginManager = None
|
||||
appSessionManager = None
|
||||
pluginLifecycleManager = None
|
||||
preemptiveCache = None
|
||||
|
||||
principals = Principal(app)
|
||||
admin_permission = Permission(RoleNeed("admin"))
|
||||
|
|
@ -61,6 +64,7 @@ import octoprint.util
|
|||
import octoprint.filemanager.storage
|
||||
import octoprint.filemanager.analysis
|
||||
import octoprint.slicing
|
||||
from octoprint.server.util.flask import PreemptiveCache
|
||||
|
||||
from . import util
|
||||
|
||||
|
|
@ -68,8 +72,9 @@ UI_API_KEY = ''.join('%02X' % ord(z) for z in uuid.uuid4().bytes)
|
|||
|
||||
versions = octoprint._version.get_versions()
|
||||
VERSION = versions['version']
|
||||
BRANCH = versions['branch'] if 'branch' in versions else None
|
||||
BRANCH = versions.get('branch', None)
|
||||
DISPLAY_VERSION = "%s (%s branch)" % (VERSION, BRANCH) if BRANCH else VERSION
|
||||
REVISION = versions.get('full-revision-id', versions.get('full', None))
|
||||
del versions
|
||||
|
||||
LOCALES = []
|
||||
|
|
@ -96,7 +101,7 @@ def load_user(id):
|
|||
else:
|
||||
sessionid = None
|
||||
|
||||
if userManager is not None:
|
||||
if userManager.enabled:
|
||||
if sessionid:
|
||||
return userManager.findUser(userid=id, session=sessionid)
|
||||
else:
|
||||
|
|
@ -124,6 +129,8 @@ class Server():
|
|||
|
||||
self._template_searchpaths = []
|
||||
|
||||
self._intermediary_server = None
|
||||
|
||||
def run(self):
|
||||
if not self._allowRoot:
|
||||
self._check_for_root()
|
||||
|
|
@ -142,6 +149,7 @@ class Server():
|
|||
global pluginManager
|
||||
global appSessionManager
|
||||
global pluginLifecycleManager
|
||||
global preemptiveCache
|
||||
global debug
|
||||
|
||||
from tornado.ioloop import IOLoop
|
||||
|
|
@ -172,6 +180,9 @@ class Server():
|
|||
sys.excepthook = exception_logger
|
||||
self._logger.info("Starting OctoPrint %s" % DISPLAY_VERSION)
|
||||
|
||||
# start the intermediary server
|
||||
self._start_intermediary_server(s)
|
||||
|
||||
# then initialize the plugin manager
|
||||
pluginManager = octoprint.plugin.plugin_manager(init=True)
|
||||
|
||||
|
|
@ -185,6 +196,7 @@ class Server():
|
|||
printer = Printer(fileManager, analysisQueue, printerProfileManager)
|
||||
appSessionManager = util.flask.AppSessionManager()
|
||||
pluginLifecycleManager = LifecycleManager(pluginManager)
|
||||
preemptiveCache = PreemptiveCache(os.path.join(s.getBaseFolder("data"), "preemptive_cache_config.yaml"))
|
||||
|
||||
def octoprint_plugin_inject_factory(name, implementation):
|
||||
if not isinstance(implementation, octoprint.plugin.OctoPrintPlugin):
|
||||
|
|
@ -199,7 +211,8 @@ class Server():
|
|||
printer=printer,
|
||||
app_session_manager=appSessionManager,
|
||||
plugin_lifecycle_manager=pluginLifecycleManager,
|
||||
data_folder=os.path.join(settings().getBaseFolder("data"), name)
|
||||
data_folder=os.path.join(settings().getBaseFolder("data"), name),
|
||||
preemptive_cache=preemptiveCache
|
||||
)
|
||||
|
||||
def settings_plugin_inject_factory(name, implementation):
|
||||
|
|
@ -278,13 +291,15 @@ class Server():
|
|||
events.DebugEventListener()
|
||||
|
||||
# setup access control
|
||||
if s.getBoolean(["accessControl", "enabled"]):
|
||||
userManagerName = s.get(["accessControl", "userManager"])
|
||||
try:
|
||||
clazz = octoprint.util.get_class(userManagerName)
|
||||
userManager = clazz()
|
||||
except AttributeError, e:
|
||||
self._logger.exception("Could not instantiate user manager %s, will run with accessControl disabled!" % userManagerName)
|
||||
userManagerName = s.get(["accessControl", "userManager"])
|
||||
try:
|
||||
clazz = octoprint.util.get_class(userManagerName)
|
||||
userManager = clazz()
|
||||
except AttributeError, e:
|
||||
self._logger.exception("Could not instantiate user manager {}, falling back to FilebasedUserManager!".format(userManagerName))
|
||||
userManager = octoprint.users.FilebasedUserManager()
|
||||
finally:
|
||||
userManager.enabled = s.getBoolean(["accessControl", "enabled"])
|
||||
|
||||
app.wsgi_app = util.ReverseProxied(
|
||||
app.wsgi_app,
|
||||
|
|
@ -308,7 +323,7 @@ class Server():
|
|||
loginManager = LoginManager()
|
||||
loginManager.session_protection = "strong"
|
||||
loginManager.user_callback = load_user
|
||||
if userManager is None:
|
||||
if not userManager.enabled:
|
||||
loginManager.anonymous_user = users.DummyUser
|
||||
principals.identity_loaders.appendleft(users.dummy_identity_loader)
|
||||
loginManager.init_app(app)
|
||||
|
|
@ -342,7 +357,7 @@ class Server():
|
|||
)
|
||||
additional_mime_types=dict(mime_type_guesser=mime_type_guesser)
|
||||
admin_validator = dict(access_validation=util.tornado.access_validation_factory(app, loginManager, util.flask.user_validator))
|
||||
no_hidden_files_validator = dict(path_validation=util.tornado.path_validation_factory(lambda path: not os.path.basename(path).startswith("."), status_code=404))
|
||||
no_hidden_files_validator = dict(path_validation=util.tornado.path_validation_factory(lambda path: not octoprint.util.is_hidden_path(path), status_code=404))
|
||||
|
||||
def joined_dict(*dicts):
|
||||
if not len(dicts):
|
||||
|
|
@ -361,7 +376,10 @@ class Server():
|
|||
# camera snapshot
|
||||
(r"/downloads/camera/current", util.tornado.UrlProxyHandler, dict(url=s.get(["webcam", "snapshot"]), as_attachment=True, access_validation=util.tornado.access_validation_factory(app, loginManager, util.flask.user_validator))),
|
||||
# generated webassets
|
||||
(r"/static/webassets/(.*)", util.tornado.LargeResponseHandler, dict(path=os.path.join(s.getBaseFolder("generated"), "webassets")))
|
||||
(r"/static/webassets/(.*)", util.tornado.LargeResponseHandler, dict(path=os.path.join(s.getBaseFolder("generated"), "webassets"))),
|
||||
# online indicators - text file with "online" as content and a transparent gif
|
||||
(r"/online.txt", util.tornado.StaticDataHandler, dict(data="online\n")),
|
||||
(r"/online.gif", util.tornado.StaticDataHandler, dict(data=bytes(base64.b64decode("R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")), content_type="image/gif"))
|
||||
]
|
||||
for name, hook in pluginManager.get_hooks("octoprint.server.http.routes").items():
|
||||
try:
|
||||
|
|
@ -414,6 +432,8 @@ class Server():
|
|||
self._logger.debug("Adding maximum body size of {size}B for {method} requests to {route})".format(**locals()))
|
||||
max_body_sizes.append((method, route, size))
|
||||
|
||||
self._stop_intermediary_server()
|
||||
|
||||
self._server = util.tornado.CustomHTTPServer(self._tornado_app, max_body_sizes=max_body_sizes, default_max_body_size=s.getInt(["server", "maxSize"]))
|
||||
self._server.listen(self._port, address=self._host)
|
||||
|
||||
|
|
@ -467,6 +487,10 @@ class Server():
|
|||
implementation.on_after_startup()
|
||||
pluginLifecycleManager.add_callback("enabled", call_on_after_startup)
|
||||
|
||||
# when we are through with that we also run our preemptive cache
|
||||
if settings().getBoolean(["devel", "cache", "preemptive"]):
|
||||
self._execute_preemptive_flask_caching(preemptiveCache)
|
||||
|
||||
import threading
|
||||
threading.Thread(target=work).start()
|
||||
ioloop.add_callback(on_after_startup)
|
||||
|
|
@ -513,7 +537,7 @@ class Server():
|
|||
if "l10n" in request.values:
|
||||
return Locale.negotiate([request.values["l10n"]], LANGUAGES)
|
||||
|
||||
if hasattr(g, "identity") and g.identity and userManager is not None:
|
||||
if hasattr(g, "identity") and g.identity and userManager.enabled:
|
||||
userid = g.identity.id
|
||||
try:
|
||||
user_language = userManager.getUserSetting(userid, ("interface", "language"))
|
||||
|
|
@ -526,7 +550,7 @@ class Server():
|
|||
if default_language is not None and not default_language == "_default" and default_language in LANGUAGES:
|
||||
return Locale.negotiate([default_language], LANGUAGES)
|
||||
|
||||
return request.accept_languages.best_match(LANGUAGES)
|
||||
return Locale.parse(request.accept_languages.best_match(LANGUAGES))
|
||||
|
||||
def _setup_logging(self, debug, logConf=None):
|
||||
defaultConfig = {
|
||||
|
|
@ -649,7 +673,9 @@ class Server():
|
|||
|
||||
# configure additional template folders for jinja2
|
||||
import jinja2
|
||||
filesystem_loader = jinja2.FileSystemLoader([])
|
||||
import octoprint.util.jinja
|
||||
filesystem_loader = octoprint.util.jinja.FilteredFileSystemLoader([],
|
||||
path_filter=lambda x: not octoprint.util.is_hidden_path(x))
|
||||
filesystem_loader.searchpath = self._template_searchpaths
|
||||
|
||||
jinja_loader = jinja2.ChoiceLoader([
|
||||
|
|
@ -661,6 +687,46 @@ class Server():
|
|||
|
||||
self._register_template_plugins()
|
||||
|
||||
def _execute_preemptive_flask_caching(self, preemptive_cache):
|
||||
from werkzeug.test import EnvironBuilder
|
||||
import time
|
||||
|
||||
# we clean up entries from our preemptive cache settings that haven't been
|
||||
# accessed longer than server.preemptiveCache.until days
|
||||
preemptive_cache_timeout = settings().getInt(["server", "preemptiveCache", "until"])
|
||||
cutoff_timestamp = time.time() - preemptive_cache_timeout * 24 * 60 * 60
|
||||
|
||||
def filter_old_entries(entry):
|
||||
return "_timestamp" in entry and entry["_timestamp"] > cutoff_timestamp
|
||||
|
||||
cache_data = preemptive_cache.clean_all_data(lambda root, entries: filter(filter_old_entries, entries))
|
||||
if not cache_data:
|
||||
return
|
||||
|
||||
def execute_caching():
|
||||
for route in sorted(cache_data.keys(), key=lambda x: (x.count("/"), x)):
|
||||
entries = reversed(sorted(cache_data[route], key=lambda x: x.get("_count", 0)))
|
||||
for kwargs in entries:
|
||||
plugin = kwargs.get("plugin", None)
|
||||
additional_request_data = kwargs.get("_additional_request_data", dict())
|
||||
kwargs = dict((k, v) for k, v in kwargs.items() if not k.startswith("_") and not k == "plugin")
|
||||
kwargs.update(additional_request_data)
|
||||
try:
|
||||
if plugin:
|
||||
self._logger.info("Preemptively caching {} (plugin {}) for {!r}".format(route, plugin, kwargs))
|
||||
else:
|
||||
self._logger.info("Preemptively caching {} for {!r}".format(route, kwargs))
|
||||
builder = EnvironBuilder(**kwargs)
|
||||
with preemptive_cache.disable_access_logging():
|
||||
app(builder.get_environ(), lambda *a, **kw: None)
|
||||
except:
|
||||
self._logger.exception("Error while trying to preemptively cache {} for {!r}".format(route, kwargs))
|
||||
|
||||
import threading
|
||||
cache_thread = threading.Thread(target=execute_caching, name="Preemptive Cache Worker")
|
||||
cache_thread.daemon = True
|
||||
cache_thread.start()
|
||||
|
||||
def _register_template_plugins(self):
|
||||
template_plugins = pluginManager.get_implementations(octoprint.plugin.TemplatePlugin)
|
||||
for plugin in template_plugins:
|
||||
|
|
@ -936,6 +1002,90 @@ class Server():
|
|||
assets.register("css_app", css_app_bundle)
|
||||
assets.register("less_app", all_less_bundle)
|
||||
|
||||
def _start_intermediary_server(self, s):
|
||||
import BaseHTTPServer
|
||||
import SimpleHTTPServer
|
||||
import threading
|
||||
|
||||
host = self._host
|
||||
port = self._port
|
||||
if host is None:
|
||||
host = s.get(["server", "host"])
|
||||
if port is None:
|
||||
port = s.getInt(["server", "port"])
|
||||
|
||||
self._logger.debug("Starting intermediary server on {}:{}".format(host, port))
|
||||
|
||||
class IntermediaryServerHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
|
||||
def __init__(self, rules=None, *args, **kwargs):
|
||||
if rules is None:
|
||||
rules = []
|
||||
self.rules = rules
|
||||
SimpleHTTPServer.SimpleHTTPRequestHandler.__init__(self, *args, **kwargs)
|
||||
|
||||
def do_GET(self):
|
||||
request_path = self.path
|
||||
if "?" in request_path:
|
||||
request_path = request_path[0:request_path.find("?")]
|
||||
|
||||
for rule in self.rules:
|
||||
path, data, content_type = rule
|
||||
if request_path == path:
|
||||
self.send_response(200)
|
||||
if content_type:
|
||||
self.send_header("Content-Type", content_type)
|
||||
self.end_headers()
|
||||
self.wfile.write(data)
|
||||
break
|
||||
else:
|
||||
self.send_response(404)
|
||||
self.wfile.write("Not found")
|
||||
|
||||
base_path = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "static"))
|
||||
rules = [
|
||||
("/", ["intermediary.html",], "text/html"),
|
||||
("/favicon.ico", ["img", "tentacle-20x20.png"], "image/png"),
|
||||
("/intermediary.gif", bytes(base64.b64decode("R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")), "image/gif")
|
||||
]
|
||||
|
||||
def contents(args):
|
||||
path = os.path.join(base_path, *args)
|
||||
if not os.path.isfile(path):
|
||||
return ""
|
||||
|
||||
with open(path, "rb") as f:
|
||||
data = f.read()
|
||||
return data
|
||||
|
||||
def process(rule):
|
||||
if len(rule) == 2:
|
||||
path, data = rule
|
||||
content_type = None
|
||||
else:
|
||||
path, data, content_type = rule
|
||||
|
||||
if isinstance(data, (list, tuple)):
|
||||
data = contents(data)
|
||||
|
||||
return path, data, content_type
|
||||
|
||||
rules = map(process, filter(lambda rule: len(rule) == 2 or len(rule) == 3, rules))
|
||||
|
||||
self._intermediary_server = BaseHTTPServer.HTTPServer((host, port), lambda *args, **kwargs: IntermediaryServerHandler(rules, *args, **kwargs))
|
||||
|
||||
thread = threading.Thread(target=self._intermediary_server.serve_forever)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
self._logger.debug("Intermediary server started")
|
||||
|
||||
def _stop_intermediary_server(self):
|
||||
if self._intermediary_server is None:
|
||||
return
|
||||
self._logger.debug("Shutting down intermediary server...")
|
||||
self._intermediary_server.shutdown()
|
||||
self._intermediary_server.server_close()
|
||||
self._logger.debug("Intermediary server shut down")
|
||||
|
||||
class LifecycleManager(object):
|
||||
def __init__(self, plugin_manager):
|
||||
|
|
|
|||
|
|
@ -111,6 +111,7 @@ def firstRunSetup():
|
|||
"pass2" in request.values.keys() and request.values["pass1"] == request.values["pass2"]:
|
||||
# configure access control
|
||||
s().setBoolean(["accessControl", "enabled"], True)
|
||||
octoprint.server.userManager.enable()
|
||||
octoprint.server.userManager.addUser(request.values["user"], request.values["pass1"], True, ["user", "admin"])
|
||||
s().setBoolean(["server", "firstRun"], False)
|
||||
elif "ac" in request.values.keys() and not request.values["ac"] in valid_boolean_trues:
|
||||
|
|
@ -120,6 +121,7 @@ def firstRunSetup():
|
|||
|
||||
octoprint.server.loginManager.anonymous_user = octoprint.users.DummyUser
|
||||
octoprint.server.principals.identity_loaders.appendleft(octoprint.users.dummy_identity_loader)
|
||||
octoprint.server.userManager.disable()
|
||||
|
||||
s().save()
|
||||
return NO_CONTENT
|
||||
|
|
@ -181,7 +183,7 @@ def performSystemAction():
|
|||
|
||||
@api.route("/login", methods=["POST"])
|
||||
def login():
|
||||
if octoprint.server.userManager is not None and "user" in request.values.keys() and "pass" in request.values.keys():
|
||||
if octoprint.server.userManager.enabled and "user" in request.values.keys() and "pass" in request.values.keys():
|
||||
username = request.values["user"]
|
||||
password = request.values["pass"]
|
||||
|
||||
|
|
@ -196,7 +198,7 @@ def login():
|
|||
user = octoprint.server.userManager.findUser(username)
|
||||
if user is not None:
|
||||
if octoprint.server.userManager.checkPassword(username, password):
|
||||
if octoprint.server.userManager is not None:
|
||||
if octoprint.server.userManager.enabled:
|
||||
user = octoprint.server.userManager.login_user(user)
|
||||
session["usersession.id"] = user.get_session()
|
||||
g.user = user
|
||||
|
|
|
|||
|
|
@ -10,10 +10,9 @@ from werkzeug.exceptions import BadRequest
|
|||
import re
|
||||
|
||||
from octoprint.settings import settings, valid_boolean_trues
|
||||
from octoprint.server import printer, NO_CONTENT
|
||||
from octoprint.server import printer, printerProfileManager, NO_CONTENT
|
||||
from octoprint.server.api import api
|
||||
from octoprint.server.util.flask import restricted_access, get_json_command_from_request
|
||||
import octoprint.util as util
|
||||
|
||||
from octoprint.printer import UnknownScript
|
||||
|
||||
|
|
@ -34,9 +33,13 @@ def printerState():
|
|||
|
||||
result = {}
|
||||
|
||||
processor = lambda x: x
|
||||
if not printerProfileManager.get_current_or_default()["heatedBed"]:
|
||||
processor = _delete_bed
|
||||
|
||||
# add temperature information
|
||||
if not "temperature" in excludes:
|
||||
result.update({"temperature": _getTemperatureData(lambda x: x)})
|
||||
result.update({"temperature": _get_temperature_data(processor)})
|
||||
|
||||
# add sd information
|
||||
if not "sd" in excludes and settings().getBoolean(["feature", "sdSupport"]):
|
||||
|
|
@ -145,14 +148,7 @@ def printerToolState():
|
|||
if not printer.is_operational():
|
||||
return make_response("Printer is not operational", 409)
|
||||
|
||||
def deleteBed(x):
|
||||
data = dict(x)
|
||||
|
||||
if "bed" in data.keys():
|
||||
del data["bed"]
|
||||
return data
|
||||
|
||||
return jsonify(_getTemperatureData(deleteBed))
|
||||
return jsonify(_get_temperature_data(_delete_bed))
|
||||
|
||||
|
||||
##~~ Heated bed
|
||||
|
|
@ -164,6 +160,9 @@ def printerBedCommand():
|
|||
if not printer.is_operational():
|
||||
return make_response("Printer is not operational", 409)
|
||||
|
||||
if not printerProfileManager.get_current_or_default()["heatedBed"]:
|
||||
return make_response("Printer does not have a heated bed", 409)
|
||||
|
||||
valid_commands = {
|
||||
"target": ["target"],
|
||||
"offset": ["offset"]
|
||||
|
|
@ -204,15 +203,10 @@ def printerBedState():
|
|||
if not printer.is_operational():
|
||||
return make_response("Printer is not operational", 409)
|
||||
|
||||
def deleteTools(x):
|
||||
data = dict(x)
|
||||
if not printerProfileManager.get_current_or_default()["heatedBed"]:
|
||||
return make_response("Printer does not have a heated bed", 409)
|
||||
|
||||
for k in data.keys():
|
||||
if k.startswith("tool"):
|
||||
del data[k]
|
||||
return data
|
||||
|
||||
data = _getTemperatureData(deleteTools)
|
||||
data = _get_temperature_data(_delete_tools)
|
||||
if isinstance(data, Response):
|
||||
return data
|
||||
else:
|
||||
|
|
@ -382,7 +376,7 @@ def getCustomControls():
|
|||
return jsonify(controls=customControls)
|
||||
|
||||
|
||||
def _getTemperatureData(filter):
|
||||
def _get_temperature_data(preprocessor):
|
||||
if not printer.is_operational():
|
||||
return make_response("Printer is not operational", 409)
|
||||
|
||||
|
|
@ -399,8 +393,23 @@ def _getTemperatureData(filter):
|
|||
limit = min(limit, len(history))
|
||||
|
||||
tempData.update({
|
||||
"history": map(lambda x: filter(x), history[-limit:])
|
||||
"history": map(lambda x: preprocessor(x), history[-limit:])
|
||||
})
|
||||
|
||||
return filter(tempData)
|
||||
return preprocessor(tempData)
|
||||
|
||||
|
||||
def _delete_tools(x):
|
||||
return _delete_from_data(x, lambda k: k.startswith("tool"))
|
||||
|
||||
|
||||
def _delete_bed(x):
|
||||
return _delete_from_data(x, lambda k: k == "bed")
|
||||
|
||||
|
||||
def _delete_from_data(x, key_matcher):
|
||||
data = dict(x)
|
||||
for k in data.keys():
|
||||
if key_matcher(k):
|
||||
del data[k]
|
||||
return data
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ from octoprint.server.util.flask import restricted_access
|
|||
@restricted_access
|
||||
@admin_permission.require(403)
|
||||
def getUsers():
|
||||
if userManager is None:
|
||||
if not userManager.enabled:
|
||||
return jsonify(SUCCESS)
|
||||
|
||||
return jsonify({"users": userManager.getAllUsers()})
|
||||
|
|
@ -33,7 +33,7 @@ def getUsers():
|
|||
@restricted_access
|
||||
@admin_permission.require(403)
|
||||
def addUser():
|
||||
if userManager is None:
|
||||
if not userManager.enabled:
|
||||
return jsonify(SUCCESS)
|
||||
|
||||
if not "application/json" in request.headers["Content-Type"]:
|
||||
|
|
@ -62,7 +62,7 @@ def addUser():
|
|||
@api.route("/users/<username>", methods=["GET"])
|
||||
@restricted_access
|
||||
def getUser(username):
|
||||
if userManager is None:
|
||||
if not userManager.enabled:
|
||||
return jsonify(SUCCESS)
|
||||
|
||||
if current_user is not None and not current_user.is_anonymous() and (current_user.get_name() == username or current_user.is_admin()):
|
||||
|
|
@ -79,7 +79,7 @@ def getUser(username):
|
|||
@restricted_access
|
||||
@admin_permission.require(403)
|
||||
def updateUser(username):
|
||||
if userManager is None:
|
||||
if not userManager.enabled:
|
||||
return jsonify(SUCCESS)
|
||||
|
||||
user = userManager.findUser(username)
|
||||
|
|
@ -110,7 +110,7 @@ def updateUser(username):
|
|||
@restricted_access
|
||||
@admin_permission.require(http_exception=403)
|
||||
def removeUser(username):
|
||||
if userManager is None:
|
||||
if not userManager.enabled:
|
||||
return jsonify(SUCCESS)
|
||||
|
||||
try:
|
||||
|
|
@ -123,7 +123,7 @@ def removeUser(username):
|
|||
@api.route("/users/<username>/password", methods=["PUT"])
|
||||
@restricted_access
|
||||
def changePasswordForUser(username):
|
||||
if userManager is None:
|
||||
if not userManager.enabled:
|
||||
return jsonify(SUCCESS)
|
||||
|
||||
if current_user is not None and not current_user.is_anonymous() and (current_user.get_name() == username or current_user.is_admin()):
|
||||
|
|
@ -151,7 +151,7 @@ def changePasswordForUser(username):
|
|||
@api.route("/users/<username>/settings", methods=["GET"])
|
||||
@restricted_access
|
||||
def getSettingsForUser(username):
|
||||
if userManager is None:
|
||||
if not userManager.enabled:
|
||||
return jsonify(SUCCESS)
|
||||
|
||||
if current_user is None or current_user.is_anonymous() or (current_user.get_name() != username and not current_user.is_admin()):
|
||||
|
|
@ -165,7 +165,7 @@ def getSettingsForUser(username):
|
|||
@api.route("/users/<username>/settings", methods=["PATCH"])
|
||||
@restricted_access
|
||||
def changeSettingsForUser(username):
|
||||
if userManager is None:
|
||||
if not userManager.enabled:
|
||||
return jsonify(SUCCESS)
|
||||
|
||||
if current_user is None or current_user.is_anonymous() or (current_user.get_name() != username and not current_user.is_admin()):
|
||||
|
|
@ -185,7 +185,7 @@ def changeSettingsForUser(username):
|
|||
@api.route("/users/<username>/apikey", methods=["DELETE"])
|
||||
@restricted_access
|
||||
def deleteApikeyForUser(username):
|
||||
if userManager is None:
|
||||
if not userManager.enabled:
|
||||
return jsonify(SUCCESS)
|
||||
|
||||
if current_user is not None and not current_user.is_anonymous() and (current_user.get_name() == username or current_user.is_admin()):
|
||||
|
|
@ -201,7 +201,7 @@ def deleteApikeyForUser(username):
|
|||
@api.route("/users/<username>/apikey", methods=["POST"])
|
||||
@restricted_access
|
||||
def generateApikeyForUser(username):
|
||||
if userManager is None:
|
||||
if not userManager.enabled:
|
||||
return jsonify(SUCCESS)
|
||||
|
||||
if current_user is not None and not current_user.is_anonymous() and (current_user.get_name() == username or current_user.is_admin()):
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ def get_user_for_apikey(apikey):
|
|||
if apikey == settings().get(["api", "key"]) or octoprint.server.appSessionManager.validate(apikey):
|
||||
# master key or an app session key was used
|
||||
return ApiUser()
|
||||
elif octoprint.server.userManager is not None:
|
||||
elif octoprint.server.userManager.enabled:
|
||||
# user key might have been used
|
||||
return octoprint.server.userManager.findUser(apikey=apikey)
|
||||
return None
|
||||
|
|
@ -143,6 +143,20 @@ def get_api_key(request):
|
|||
return None
|
||||
|
||||
|
||||
def get_plugin_hash():
|
||||
from octoprint.plugin import plugin_manager
|
||||
|
||||
plugin_signature = lambda impl: "{}:{}".format(impl._identifier, impl._plugin_version)
|
||||
template_plugins = map(plugin_signature, plugin_manager().get_implementations(octoprint.plugin.TemplatePlugin))
|
||||
asset_plugins = map(plugin_signature, plugin_manager().get_implementations(octoprint.plugin.AssetPlugin))
|
||||
ui_plugins = sorted(set(template_plugins + asset_plugins))
|
||||
|
||||
import hashlib
|
||||
plugin_hash = hashlib.sha1()
|
||||
plugin_hash.update(",".join(ui_plugins))
|
||||
return plugin_hash.hexdigest()
|
||||
|
||||
|
||||
#~~ reverse proxy compatible WSGI middleware
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import flask.ext.assets
|
|||
import webassets.updater
|
||||
import webassets.utils
|
||||
import functools
|
||||
import contextlib
|
||||
import time
|
||||
import uuid
|
||||
import threading
|
||||
|
|
@ -221,7 +222,7 @@ def fix_webassets_filtertool():
|
|||
#~~ passive login helper
|
||||
|
||||
def passive_login():
|
||||
if octoprint.server.userManager is not None:
|
||||
if octoprint.server.userManager.enabled:
|
||||
user = octoprint.server.userManager.login_user(flask.ext.login.current_user)
|
||||
else:
|
||||
user = flask.ext.login.current_user
|
||||
|
|
@ -313,7 +314,7 @@ class LessSimpleCache(BaseCache):
|
|||
|
||||
_cache = LessSimpleCache()
|
||||
|
||||
def cached(timeout=5 * 60, key=lambda: "view/%s" % flask.request.path, unless=None, refreshif=None, unless_response=None):
|
||||
def cached(timeout=5 * 60, key=lambda: "view:%s" % flask.request.path, unless=None, refreshif=None, unless_response=None):
|
||||
def decorator(f):
|
||||
@functools.wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
|
|
@ -330,16 +331,17 @@ def cached(timeout=5 * 60, key=lambda: "view/%s" % flask.request.path, unless=No
|
|||
return f(*args, **kwargs)
|
||||
|
||||
cache_key = key()
|
||||
rv = _cache.get(cache_key)
|
||||
|
||||
# only take the value from the cache if we are not required to refresh it from the wrapped function
|
||||
if not callable(refreshif) or not refreshif():
|
||||
rv = _cache.get(cache_key)
|
||||
if rv is not None:
|
||||
logger.debug("Serving entry for {path} from cache".format(path=flask.request.path))
|
||||
return rv
|
||||
if rv is not None and (not callable(refreshif) or not refreshif(rv)):
|
||||
logger.debug("Serving entry for {path} from cache".format(path=flask.request.path))
|
||||
if not "X-From-Cache" in rv.headers:
|
||||
rv.headers["X-From-Cache"] = "true"
|
||||
return rv
|
||||
|
||||
# get value from wrapped function
|
||||
logger.debug("No cache entry or refreshing cache for {path}, calling wrapped function".format(path=flask.request.path))
|
||||
logger.debug("No cache entry or refreshing cache for {path} (key: {key}), calling wrapped function".format(path=flask.request.path, key=cache_key))
|
||||
rv = f(*args, **kwargs)
|
||||
|
||||
# do not store if the "unless_response" condition is true
|
||||
|
|
@ -377,6 +379,222 @@ def cache_check_response_headers(response):
|
|||
return False
|
||||
|
||||
|
||||
class PreemptiveCache(object):
|
||||
|
||||
def __init__(self, cachefile):
|
||||
self.cachefile = cachefile
|
||||
|
||||
self._lock = threading.RLock()
|
||||
self._logger = logging.getLogger(__name__ + "." + self.__class__.__name__)
|
||||
self._log_access = True
|
||||
|
||||
def record(self, data, unless=None):
|
||||
if callable(unless) and unless():
|
||||
return
|
||||
|
||||
entry_data = data
|
||||
if callable(entry_data):
|
||||
entry_data = entry_data()
|
||||
|
||||
if entry_data is not None:
|
||||
from flask import request
|
||||
self.add_data(request.path, entry_data)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def disable_access_logging(self):
|
||||
with self._lock:
|
||||
self._log_access = False
|
||||
yield
|
||||
self._log_access = True
|
||||
|
||||
def clean_all_data(self, cleanup_function):
|
||||
assert callable(cleanup_function)
|
||||
|
||||
with self._lock:
|
||||
all_data = self.get_all_data()
|
||||
for root, entries in all_data.items():
|
||||
old_count = len(entries)
|
||||
entries = cleanup_function(root, entries)
|
||||
if not entries:
|
||||
del all_data[root]
|
||||
self._logger.debug("Removed root {} from preemptive cache".format(root))
|
||||
elif len(entries) < old_count:
|
||||
all_data[root] = entries
|
||||
self._logger.debug("Removed {} from preemptive cache for root {}".format(old_count - len(entries), root))
|
||||
self.set_all_data(all_data)
|
||||
|
||||
return all_data
|
||||
|
||||
def get_all_data(self):
|
||||
import yaml
|
||||
|
||||
cache_data = None
|
||||
with self._lock:
|
||||
try:
|
||||
with open(self.cachefile, "r") as f:
|
||||
cache_data = yaml.safe_load(f)
|
||||
except IOError as e:
|
||||
import errno
|
||||
if e.errno != errno.ENOENT:
|
||||
raise
|
||||
except:
|
||||
self._logger.exception("Error while reading {}".format(self.cachefile))
|
||||
|
||||
if cache_data is None:
|
||||
cache_data = dict()
|
||||
|
||||
return cache_data
|
||||
|
||||
def get_data(self, root):
|
||||
cache_data = self.get_all_data()
|
||||
return cache_data.get(root, dict())
|
||||
|
||||
def set_all_data(self, data):
|
||||
from octoprint.util import atomic_write
|
||||
import yaml
|
||||
|
||||
with self._lock:
|
||||
try:
|
||||
with atomic_write(self.cachefile, "wb") as handle:
|
||||
yaml.safe_dump(data, handle,default_flow_style=False, indent=" ", allow_unicode=True)
|
||||
except:
|
||||
self._logger.exception("Error while writing {}".format(self.cachefile))
|
||||
|
||||
def set_data(self, root, data):
|
||||
with self._lock:
|
||||
all_data = self.get_all_data()
|
||||
all_data[root] = data
|
||||
self.set_all_data(all_data)
|
||||
|
||||
def add_data(self, root, data):
|
||||
from octoprint.util import dict_filter
|
||||
|
||||
def strip_ignored(d):
|
||||
return dict_filter(d, lambda k, v: not k.startswith("_"))
|
||||
|
||||
def compare(a, b):
|
||||
return set(strip_ignored(a).items()) == set(strip_ignored(b).items())
|
||||
|
||||
def split_matched_and_unmatched(entry, entries):
|
||||
matched = []
|
||||
unmatched = []
|
||||
|
||||
for e in entries:
|
||||
if compare(e, entry):
|
||||
matched.append(e)
|
||||
else:
|
||||
unmatched.append(e)
|
||||
|
||||
return matched, unmatched
|
||||
|
||||
with self._lock:
|
||||
cache_data = self.get_all_data()
|
||||
|
||||
if not root in cache_data:
|
||||
cache_data[root] = []
|
||||
|
||||
existing, other = split_matched_and_unmatched(data, cache_data[root])
|
||||
|
||||
def get_newest(entries):
|
||||
result = None
|
||||
for entry in entries:
|
||||
if "_timestamp" in entry and (result is None or ("_timestamp" in entry and result["_timestamp"] < entry["_timestamp"])):
|
||||
result = entry
|
||||
return result
|
||||
|
||||
to_persist = get_newest(existing)
|
||||
if not to_persist:
|
||||
import copy
|
||||
to_persist = copy.deepcopy(data)
|
||||
to_persist["_timestamp"] = time.time()
|
||||
to_persist["_count"] = 1
|
||||
self._logger.info("Adding entry for {} and {!r}".format(root, to_persist))
|
||||
elif self._log_access:
|
||||
to_persist["_timestamp"] = time.time()
|
||||
to_persist["_count"] = to_persist.get("_count", 0) + 1
|
||||
self._logger.debug("Updating timestamp and counter for {} and {!r}".format(root, data))
|
||||
else:
|
||||
self._logger.debug("Not updating timestamp and counter for {} and {!r}, currently flagged as disabled".format(root, data))
|
||||
|
||||
self.set_data(root, [to_persist] + other)
|
||||
|
||||
|
||||
def preemptively_cached(cache, data, unless=None):
|
||||
def decorator(f):
|
||||
@functools.wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
cache.record(data, unless=unless)
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
return decorator
|
||||
|
||||
|
||||
def etagged(etag):
|
||||
def decorator(f):
|
||||
@functools.wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
rv = f(*args, **kwargs)
|
||||
if isinstance(rv, flask.Response):
|
||||
result = etag
|
||||
if callable(result):
|
||||
result = result(rv)
|
||||
if result:
|
||||
rv.set_etag(result)
|
||||
return rv
|
||||
return decorated_function
|
||||
return decorator
|
||||
|
||||
|
||||
def lastmodified(date):
|
||||
def decorator(f):
|
||||
@functools.wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
rv = f(*args, **kwargs)
|
||||
if not "Last-Modified" in rv.headers:
|
||||
result = date
|
||||
if callable(result):
|
||||
result = result(rv)
|
||||
|
||||
if not isinstance(result, basestring):
|
||||
from werkzeug.http import http_date
|
||||
result = http_date(result)
|
||||
|
||||
if result:
|
||||
rv.headers["Last-Modified"] = result
|
||||
return rv
|
||||
return decorated_function
|
||||
return decorator
|
||||
|
||||
|
||||
def conditional(condition, met):
|
||||
def decorator(f):
|
||||
@functools.wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if callable(condition) and condition():
|
||||
# condition has been met, return met-response
|
||||
rv = met
|
||||
if callable(met):
|
||||
rv = met()
|
||||
return rv
|
||||
|
||||
# condition hasn't been met, call decorated function
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
return decorator
|
||||
|
||||
|
||||
def check_etag(etag):
|
||||
return flask.request.method in ("GET", "HEAD") and \
|
||||
flask.request.if_none_match and \
|
||||
etag in flask.request.if_none_match
|
||||
|
||||
|
||||
def check_lastmodified(lastmodified):
|
||||
return flask.request.method in ("GET", "HEAD") and \
|
||||
flask.request.if_modified_since and \
|
||||
lastmodified >= flask.request.if_modified_since
|
||||
|
||||
|
||||
def add_non_caching_response_headers(response):
|
||||
response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0"
|
||||
response.headers["Pragma"] = "no-cache"
|
||||
|
|
@ -482,7 +700,7 @@ def restricted_access(func):
|
|||
@functools.wraps(func)
|
||||
def decorated_view(*args, **kwargs):
|
||||
# if OctoPrint hasn't been set up yet, abort
|
||||
if settings().getBoolean(["server", "firstRun"]) and (octoprint.server.userManager is None or not octoprint.server.userManager.hasBeenCustomized()):
|
||||
if settings().getBoolean(["server", "firstRun"]) and settings().getBoolean(["accessControl", "enabled"]) and (octoprint.server.userManager is None or not octoprint.server.userManager.hasBeenCustomized()):
|
||||
return flask.make_response("OctoPrint isn't setup yet", 403)
|
||||
|
||||
apikey = octoprint.server.util.get_api_key(flask.request)
|
||||
|
|
|
|||
|
|
@ -927,6 +927,19 @@ class UrlProxyHandler(tornado.web.RequestHandler):
|
|||
return "%s%s" % (self._basename, extension)
|
||||
|
||||
|
||||
class StaticDataHandler(tornado.web.RequestHandler):
|
||||
def initialize(self, data="", content_type="text/plain"):
|
||||
self.data = data
|
||||
self.content_type = content_type
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
self.set_status(200)
|
||||
self.set_header("Content-Type", self.content_type)
|
||||
self.write(self.data)
|
||||
self.flush()
|
||||
self.finish()
|
||||
|
||||
|
||||
#~~ Factory method for creating Flask access validation wrappers from the Tornado request context
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agp
|
|||
__copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms of the AGPLv3 License"
|
||||
|
||||
import logging
|
||||
import os
|
||||
import watchdog.events
|
||||
|
||||
import octoprint.filemanager
|
||||
|
|
@ -14,6 +15,7 @@ import octoprint.util
|
|||
|
||||
|
||||
class GcodeWatchdogHandler(watchdog.events.PatternMatchingEventHandler):
|
||||
|
||||
"""
|
||||
Takes care of automatically "uploading" files that get added to the watched folder.
|
||||
"""
|
||||
|
|
@ -27,40 +29,42 @@ class GcodeWatchdogHandler(watchdog.events.PatternMatchingEventHandler):
|
|||
self._printer = printer
|
||||
|
||||
def _upload(self, path):
|
||||
import os
|
||||
file_wrapper = octoprint.filemanager.util.DiskFileWrapper(os.path.basename(path), path)
|
||||
|
||||
# determine current job
|
||||
currentFilename = None
|
||||
currentOrigin = None
|
||||
currentJob = self._printer.get_current_job()
|
||||
if currentJob is not None and "file" in currentJob.keys():
|
||||
currentJobFile = currentJob["file"]
|
||||
if "name" in currentJobFile.keys() and "origin" in currentJobFile.keys():
|
||||
currentFilename = currentJobFile["name"]
|
||||
currentOrigin = currentJobFile["origin"]
|
||||
|
||||
# determine future filename of file to be uploaded, abort if it can't be uploaded
|
||||
try:
|
||||
futureFilename = self._file_manager.sanitize_name(octoprint.filemanager.FileDestinations.LOCAL, file_wrapper.filename)
|
||||
except:
|
||||
futureFilename = None
|
||||
if futureFilename is None or (len(self._file_manager.registered_slicers) == 0 and not octoprint.filemanager.valid_file_type(futureFilename)):
|
||||
return
|
||||
file_wrapper = octoprint.filemanager.util.DiskFileWrapper(os.path.basename(path), path)
|
||||
|
||||
# prohibit overwriting currently selected file while it's being printed
|
||||
if futureFilename == currentFilename and currentOrigin == octoprint.filemanager.FileDestinations.LOCAL and self._printer.is_printing() or self._printer.is_paused():
|
||||
return
|
||||
# determine current job
|
||||
currentFilename = None
|
||||
currentOrigin = None
|
||||
currentJob = self._printer.get_current_job()
|
||||
if currentJob is not None and "file" in currentJob.keys():
|
||||
currentJobFile = currentJob["file"]
|
||||
if "name" in currentJobFile.keys() and "origin" in currentJobFile.keys():
|
||||
currentFilename = currentJobFile["name"]
|
||||
currentOrigin = currentJobFile["origin"]
|
||||
|
||||
self._file_manager.add_file(octoprint.filemanager.FileDestinations.LOCAL,
|
||||
file_wrapper.filename,
|
||||
file_wrapper,
|
||||
allow_overwrite=True)
|
||||
if os.path.exists(path):
|
||||
# determine future filename of file to be uploaded, abort if it can't be uploaded
|
||||
try:
|
||||
os.remove(path)
|
||||
futureFilename = self._file_manager.sanitize_name(octoprint.filemanager.FileDestinations.LOCAL, file_wrapper.filename)
|
||||
except:
|
||||
self._logger.exception("Error while trying to clear a file from the watched folder")
|
||||
futureFilename = None
|
||||
if futureFilename is None or (len(self._file_manager.registered_slicers) == 0 and not octoprint.filemanager.valid_file_type(futureFilename)):
|
||||
return
|
||||
|
||||
# prohibit overwriting currently selected file while it's being printed
|
||||
if futureFilename == currentFilename and currentOrigin == octoprint.filemanager.FileDestinations.LOCAL and self._printer.is_printing() or self._printer.is_paused():
|
||||
return
|
||||
|
||||
self._file_manager.add_file(octoprint.filemanager.FileDestinations.LOCAL,
|
||||
file_wrapper.filename,
|
||||
file_wrapper,
|
||||
allow_overwrite=True)
|
||||
if os.path.exists(path):
|
||||
try:
|
||||
os.remove(path)
|
||||
except:
|
||||
self._logger.exception("Error while trying to clear a file from the watched folder")
|
||||
except:
|
||||
self._logger.exception("There was an error while processing the file {} in the watched folder".format(path))
|
||||
|
||||
def on_created(self, event):
|
||||
self._upload(event.src_path)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@ from flask import request, g, url_for, make_response, render_template, send_from
|
|||
import octoprint.plugin
|
||||
|
||||
from octoprint.server import app, userManager, pluginManager, gettext, \
|
||||
debug, LOCALES, VERSION, DISPLAY_VERSION, UI_API_KEY, BRANCH
|
||||
debug, LOCALES, VERSION, DISPLAY_VERSION, UI_API_KEY, BRANCH, preemptiveCache, \
|
||||
NOT_MODIFIED
|
||||
from octoprint.settings import settings
|
||||
|
||||
import re
|
||||
|
|
@ -27,18 +28,23 @@ _valid_id_re = re.compile("[a-z_]+")
|
|||
_valid_div_re = re.compile("[a-zA-Z_-]+")
|
||||
|
||||
@app.route("/")
|
||||
@util.flask.preemptively_cached(cache=preemptiveCache,
|
||||
data=lambda: dict(path=request.path, base_url=request.url_root, query_string="l10n={}".format(g.locale.language)) if g.locale else "en",
|
||||
unless=lambda: request.url_root in settings().get(["server", "preemptiveCache", "exceptions"]))
|
||||
@util.flask.conditional(lambda: _check_etag_and_lastmodified_for_index(), NOT_MODIFIED)
|
||||
@util.flask.cached(timeout=-1,
|
||||
refreshif=lambda: util.flask.cache_check_headers() or "_refresh" in request.values,
|
||||
key=lambda: "view/%s/%s" % (request.path, g.locale),
|
||||
unless_response=util.flask.cache_check_response_headers)
|
||||
refreshif=lambda cached: _validate_cache_for_index(cached),
|
||||
key=lambda: "view:{}:{}".format(request.base_url, g.locale.language if g.locale else "en"),
|
||||
unless_response=lambda response: util.flask.cache_check_response_headers(response))
|
||||
@util.flask.etagged(lambda _: _compute_etag_for_index())
|
||||
@util.flask.lastmodified(lambda _: _compute_date_for_index())
|
||||
def index():
|
||||
|
||||
#~~ a bunch of settings
|
||||
|
||||
enable_gcodeviewer = settings().getBoolean(["gcodeViewer", "enabled"])
|
||||
enable_timelapse = (settings().get(["webcam", "snapshot"]) and settings().get(["webcam", "ffmpeg"]))
|
||||
enable_systemmenu = settings().get(["system"]) is not None and settings().get(["system", "actions"]) is not None and len(settings().get(["system", "actions"])) > 0
|
||||
enable_accesscontrol = userManager is not None
|
||||
enable_systemmenu = settings().get(["system"]) is not None and settings().get(["system", "actions"]) is not None
|
||||
enable_accesscontrol = userManager.enabled
|
||||
preferred_stylesheet = settings().get(["devel", "stylesheet"])
|
||||
locales = dict((l.language, dict(language=l.language, display=l.display_name, english=l.english_name)) for l in LOCALES)
|
||||
|
||||
|
|
@ -275,11 +281,11 @@ def index():
|
|||
|
||||
#~~ prepare full set of template vars for rendering
|
||||
|
||||
first_run = settings().getBoolean(["server", "firstRun"]) and (userManager is None or not userManager.hasBeenCustomized())
|
||||
first_run = settings().getBoolean(["server", "firstRun"]) and userManager.enabled and not userManager.hasBeenCustomized()
|
||||
render_kwargs = dict(
|
||||
webcamStream=settings().get(["webcam", "stream"]),
|
||||
enableTemperatureGraph=settings().get(["feature", "temperatureGraph"]),
|
||||
enableAccessControl=userManager is not None,
|
||||
enableAccessControl=userManager.enabled,
|
||||
enableSdSupport=settings().get(["feature", "sdSupport"]),
|
||||
firstRun=first_run,
|
||||
debug=debug,
|
||||
|
|
@ -297,13 +303,10 @@ def index():
|
|||
|
||||
#~~ render!
|
||||
|
||||
import datetime
|
||||
|
||||
response = make_response(render_template(
|
||||
"index.jinja2",
|
||||
**render_kwargs
|
||||
))
|
||||
response.headers["Last-Modified"] = datetime.datetime.now()
|
||||
|
||||
if first_run:
|
||||
response = util.flask.add_non_caching_response_headers(response)
|
||||
|
|
@ -354,6 +357,7 @@ def _process_template_configs(name, implementation, configs, rules):
|
|||
|
||||
return includes
|
||||
|
||||
|
||||
def _process_template_config(name, implementation, rule, config=None, counter=1):
|
||||
if "mandatory" in rule:
|
||||
for mandatory in rule["mandatory"]:
|
||||
|
|
@ -396,78 +400,23 @@ def _process_template_config(name, implementation, rule, config=None, counter=1)
|
|||
|
||||
return data
|
||||
|
||||
|
||||
@app.route("/robots.txt")
|
||||
@util.flask.cached(timeout=-1)
|
||||
def robotsTxt():
|
||||
return send_from_directory(app.static_folder, "robots.txt")
|
||||
|
||||
|
||||
@app.route("/i18n/<string:locale>/<string:domain>.js")
|
||||
@util.flask.cached(timeout=-1,
|
||||
refreshif=lambda: util.flask.cache_check_headers() or "_refresh" in request.values,
|
||||
key=lambda: "view/%s/%s" % (request.path, g.locale))
|
||||
@util.flask.conditional(lambda: _check_etag_and_lastmodified_for_i18n(), NOT_MODIFIED)
|
||||
@util.flask.etagged(lambda _: _compute_etag_for_i18n(request.view_args["locale"], request.view_args["domain"]))
|
||||
@util.flask.lastmodified(lambda _: _compute_date_for_i18n(request.view_args["locale"], request.view_args["domain"]))
|
||||
def localeJs(locale, domain):
|
||||
messages = dict()
|
||||
plural_expr = None
|
||||
|
||||
if locale != "en":
|
||||
from flask import _request_ctx_stack
|
||||
from babel.messages.pofile import read_po
|
||||
|
||||
def messages_from_po(base_path, locale, domain):
|
||||
path = os.path.join(base_path, locale)
|
||||
if not os.path.isdir(path):
|
||||
return None, None
|
||||
|
||||
path = os.path.join(path, "LC_MESSAGES", "{domain}.po".format(**locals()))
|
||||
if not os.path.isfile(path):
|
||||
return None, None
|
||||
|
||||
messages = dict()
|
||||
with file(path) as f:
|
||||
catalog = read_po(f, locale=locale, domain=domain)
|
||||
|
||||
for message in catalog:
|
||||
message_id = message.id
|
||||
if isinstance(message_id, (list, tuple)):
|
||||
message_id = message_id[0]
|
||||
messages[message_id] = message.string
|
||||
|
||||
return messages, catalog.plural_expr
|
||||
|
||||
user_base_path = os.path.join(settings().getBaseFolder("translations"))
|
||||
user_plugin_path = os.path.join(user_base_path, "_plugins")
|
||||
|
||||
# plugin translations
|
||||
plugins = octoprint.plugin.plugin_manager().enabled_plugins
|
||||
for name, plugin in plugins.items():
|
||||
dirs = [os.path.join(user_plugin_path, name), os.path.join(plugin.location, 'translations')]
|
||||
for dirname in dirs:
|
||||
if not os.path.isdir(dirname):
|
||||
continue
|
||||
|
||||
plugin_messages, _ = messages_from_po(dirname, locale, domain)
|
||||
|
||||
if plugin_messages is not None:
|
||||
messages = octoprint.util.dict_merge(messages, plugin_messages)
|
||||
_logger.debug("Using translation folder {dirname} for locale {locale} of plugin {name}".format(**locals()))
|
||||
break
|
||||
else:
|
||||
_logger.debug("No translations for locale {locale} for plugin {name}".format(**locals()))
|
||||
|
||||
# core translations
|
||||
ctx = _request_ctx_stack.top
|
||||
base_path = os.path.join(ctx.app.root_path, "translations")
|
||||
|
||||
dirs = [user_base_path, base_path]
|
||||
for dirname in dirs:
|
||||
core_messages, plural_expr = messages_from_po(dirname, locale, domain)
|
||||
|
||||
if core_messages is not None:
|
||||
messages = octoprint.util.dict_merge(messages, core_messages)
|
||||
_logger.debug("Using translation folder {dirname} for locale {locale} of core translations".format(**locals()))
|
||||
break
|
||||
else:
|
||||
_logger.debug("No core translations for locale {locale}".format(**locals()))
|
||||
messages, plural_expr = _get_translations(locale, domain)
|
||||
|
||||
catalog = dict(
|
||||
messages=messages,
|
||||
|
|
@ -485,3 +434,192 @@ def plugin_assets(name, filename):
|
|||
return redirect(url_for("plugin." + name + ".static", filename=filename))
|
||||
|
||||
|
||||
def _compute_etag_for_index(files=None, lastmodified=None):
|
||||
if files is None:
|
||||
files = _files_for_index()
|
||||
if lastmodified is None:
|
||||
lastmodified = _compute_date(files)
|
||||
if lastmodified and not isinstance(lastmodified, basestring):
|
||||
from werkzeug.http import http_date
|
||||
lastmodified = http_date(lastmodified)
|
||||
|
||||
from octoprint import __version__
|
||||
from octoprint.server import UI_API_KEY
|
||||
|
||||
import hashlib
|
||||
hash = hashlib.sha1()
|
||||
hash.update(__version__)
|
||||
hash.update(UI_API_KEY)
|
||||
hash.update(",".join(sorted(files)))
|
||||
if lastmodified:
|
||||
hash.update(lastmodified)
|
||||
return hash.hexdigest()
|
||||
|
||||
|
||||
def _compute_etag_for_i18n(locale, domain, files=None, lastmodified=None):
|
||||
if files is None:
|
||||
files = _get_all_translationfiles(locale, domain)
|
||||
if lastmodified is None:
|
||||
lastmodified = _compute_date(files)
|
||||
if lastmodified and not isinstance(lastmodified, basestring):
|
||||
from werkzeug.http import http_date
|
||||
lastmodified = http_date(lastmodified)
|
||||
|
||||
import hashlib
|
||||
hash = hashlib.sha1()
|
||||
hash.update(",".join(sorted(files)))
|
||||
if lastmodified:
|
||||
hash.update(lastmodified)
|
||||
return hash.hexdigest()
|
||||
|
||||
|
||||
def _compute_date_for_i18n(locale, domain):
|
||||
return _compute_date(_get_all_translationfiles(locale, domain))
|
||||
|
||||
|
||||
def _compute_date_for_index():
|
||||
return _compute_date(_files_for_index())
|
||||
|
||||
|
||||
def _validate_cache_for_index(cached):
|
||||
no_cache_headers = util.flask.cache_check_headers()
|
||||
refresh_flag = "_refresh" in request.values
|
||||
etag_different = _compute_etag_for_index() != cached.get_etag()[0]
|
||||
|
||||
return no_cache_headers or refresh_flag or etag_different
|
||||
|
||||
|
||||
def _files_for_index():
|
||||
"""
|
||||
Collects all paths of files that the index page depends on.
|
||||
|
||||
The relevant files are:
|
||||
|
||||
* all jinja2 templates: they might be used within the index page, so
|
||||
any changes here change the rendering outcome
|
||||
* all defined assets: if one of them changes, the webassets bundle will
|
||||
be regenerated and hence the URL included in the cached page won't be
|
||||
valid anymore
|
||||
* all translation files used for our current locale: if any of those change
|
||||
we also need to re-render
|
||||
"""
|
||||
|
||||
templates = _get_all_templates()
|
||||
assets = _get_all_assets()
|
||||
translations = _get_all_translationfiles(g.locale.language if g.locale else "en", "messages")
|
||||
return sorted(set(templates + assets + translations))
|
||||
|
||||
|
||||
def _compute_date(files):
|
||||
from datetime import datetime
|
||||
timestamps = map(lambda path: os.stat(path).st_mtime, files)
|
||||
max_timestamp = max(*timestamps) if timestamps else None
|
||||
if max_timestamp:
|
||||
# we set the micros to 0 since microseconds are not speced for HTTP
|
||||
max_timestamp = datetime.fromtimestamp(max_timestamp).replace(microsecond=0)
|
||||
return max_timestamp
|
||||
|
||||
|
||||
def _check_etag_and_lastmodified_for_index():
|
||||
files = _files_for_index()
|
||||
lastmodified = _compute_date(files)
|
||||
lastmodified_ok = util.flask.check_lastmodified(lastmodified)
|
||||
etag_ok = util.flask.check_etag(_compute_etag_for_index(files, lastmodified))
|
||||
return etag_ok and lastmodified_ok
|
||||
|
||||
|
||||
def _check_etag_and_lastmodified_for_i18n():
|
||||
locale = request.view_args["locale"]
|
||||
domain = request.view_args["domain"]
|
||||
|
||||
etag_ok = util.flask.check_etag(_compute_etag_for_i18n(request.view_args["locale"], request.view_args["domain"]))
|
||||
|
||||
lastmodified = _compute_date_for_i18n(locale, domain)
|
||||
lastmodified_ok = lastmodified is None or util.flask.check_lastmodified(lastmodified)
|
||||
|
||||
return etag_ok and lastmodified_ok
|
||||
|
||||
|
||||
def _get_all_templates():
|
||||
from octoprint.util.jinja import get_all_template_paths
|
||||
return get_all_template_paths(app.jinja_loader)
|
||||
|
||||
|
||||
def _get_all_assets():
|
||||
from octoprint.util.jinja import get_all_asset_paths
|
||||
return get_all_asset_paths(app.jinja_env.assets_environment)
|
||||
|
||||
|
||||
def _get_all_translationfiles(locale, domain):
|
||||
from flask import _request_ctx_stack
|
||||
|
||||
def get_po_path(basedir, locale, domain):
|
||||
path = os.path.join(basedir, locale)
|
||||
if not os.path.isdir(path):
|
||||
return None
|
||||
|
||||
path = os.path.join(path, "LC_MESSAGES", "{domain}.po".format(**locals()))
|
||||
if not os.path.isfile(path):
|
||||
return None
|
||||
|
||||
return path
|
||||
|
||||
po_files = []
|
||||
|
||||
user_base_path = os.path.join(settings().getBaseFolder("translations"))
|
||||
user_plugin_path = os.path.join(user_base_path, "_plugins")
|
||||
|
||||
# plugin translations
|
||||
plugins = octoprint.plugin.plugin_manager().enabled_plugins
|
||||
for name, plugin in plugins.items():
|
||||
dirs = [os.path.join(user_plugin_path, name), os.path.join(plugin.location, 'translations')]
|
||||
for dirname in dirs:
|
||||
if not os.path.isdir(dirname):
|
||||
continue
|
||||
|
||||
po_file = get_po_path(dirname, locale, domain)
|
||||
if po_file:
|
||||
po_files.append(po_file)
|
||||
break
|
||||
|
||||
# core translations
|
||||
ctx = _request_ctx_stack.top
|
||||
base_path = os.path.join(ctx.app.root_path, "translations")
|
||||
|
||||
dirs = [user_base_path, base_path]
|
||||
for dirname in dirs:
|
||||
po_file = get_po_path(dirname, locale, domain)
|
||||
if po_file:
|
||||
po_files.append(po_file)
|
||||
break
|
||||
|
||||
return po_files
|
||||
|
||||
|
||||
def _get_translations(locale, domain):
|
||||
from babel.messages.pofile import read_po
|
||||
from octoprint.util import dict_merge
|
||||
|
||||
messages = dict()
|
||||
plural_expr = None
|
||||
|
||||
def messages_from_po(path, locale, domain):
|
||||
messages = dict()
|
||||
with file(path) as f:
|
||||
catalog = read_po(f, locale=locale, domain=domain)
|
||||
|
||||
for message in catalog:
|
||||
message_id = message.id
|
||||
if isinstance(message_id, (list, tuple)):
|
||||
message_id = message_id[0]
|
||||
messages[message_id] = message.string
|
||||
|
||||
return messages, catalog.plural_expr
|
||||
|
||||
po_files = _get_all_translationfiles(locale, domain)
|
||||
for po_file in po_files:
|
||||
po_messages, plural_expr = messages_from_po(po_file, locale, domain)
|
||||
if po_messages is not None:
|
||||
messages = dict_merge(messages, po_messages)
|
||||
|
||||
return messages, plural_expr
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ import logging
|
|||
import re
|
||||
import uuid
|
||||
|
||||
from octoprint.util import atomic_write
|
||||
from octoprint.util import atomic_write, is_hidden_path
|
||||
|
||||
_APPNAME = "OctoPrint"
|
||||
|
||||
|
|
@ -114,6 +114,10 @@ default_settings = {
|
|||
"diskspace": {
|
||||
"warning": 500 * 1024 * 1024, # 500 MB
|
||||
"critical": 200 * 1024 * 1024, # 200 MB
|
||||
},
|
||||
"preemptiveCache": {
|
||||
"exceptions": [],
|
||||
"until": 7
|
||||
}
|
||||
},
|
||||
"webcam": {
|
||||
|
|
@ -253,16 +257,18 @@ default_settings = {
|
|||
},
|
||||
"scripts": {
|
||||
"gcode": {
|
||||
"afterPrintCancelled": "; disable motors\nM84\n\n;disable all heaters\n{% snippet 'disable_hotends' %}\nM140 S0\n\n;disable fan\nM106 S0",
|
||||
"afterPrintCancelled": "; disable motors\nM84\n\n;disable all heaters\n{% snippet 'disable_hotends' %}\n{% snippet 'disable_bed' %}\n;disable fan\nM106 S0",
|
||||
"snippets": {
|
||||
"disable_hotends": "{% for tool in range(printer_profile.extruder.count) %}M104 T{{ tool }} S0\n{% endfor %}"
|
||||
"disable_hotends": "{% for tool in range(printer_profile.extruder.count) %}M104 T{{ tool }} S0\n{% endfor %}",
|
||||
"disable_bed": "{% if printer_profile.heatedBed %}M140 S0\n{% endif %}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"devel": {
|
||||
"stylesheet": "css",
|
||||
"cache": {
|
||||
"enabled": True
|
||||
"enabled": True,
|
||||
"preemptive": True
|
||||
},
|
||||
"webassets": {
|
||||
"minify": False,
|
||||
|
|
@ -295,7 +301,8 @@ default_settings = {
|
|||
"commandBuffer": 4,
|
||||
"sendWait": True,
|
||||
"waitInterval": 1.0,
|
||||
"supportM112": True
|
||||
"supportM112": True,
|
||||
"echoOnM117": True
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -405,10 +412,12 @@ class Settings(object):
|
|||
return folder
|
||||
|
||||
def _init_script_templating(self):
|
||||
from jinja2 import Environment, BaseLoader, FileSystemLoader, ChoiceLoader, TemplateNotFound
|
||||
from jinja2.nodes import Include, Const
|
||||
from jinja2 import Environment, BaseLoader, ChoiceLoader, TemplateNotFound
|
||||
from jinja2.nodes import Include
|
||||
from jinja2.ext import Extension
|
||||
|
||||
from octoprint.util.jinja import FilteredFileSystemLoader
|
||||
|
||||
class SnippetExtension(Extension):
|
||||
tags = {"snippet"}
|
||||
fields = Include.fields
|
||||
|
|
@ -497,10 +506,14 @@ class Settings(object):
|
|||
else:
|
||||
return template
|
||||
|
||||
file_system_loader = FileSystemLoader(self.getBaseFolder("scripts"))
|
||||
path_filter = lambda path: not is_hidden_path(path)
|
||||
file_system_loader = FilteredFileSystemLoader(self.getBaseFolder("scripts"),
|
||||
path_filter=path_filter)
|
||||
settings_loader = SettingsScriptLoader(self)
|
||||
choice_loader = ChoiceLoader([file_system_loader, settings_loader])
|
||||
select_loader = SelectLoader(choice_loader, dict(bundled=settings_loader, file=file_system_loader))
|
||||
select_loader = SelectLoader(choice_loader,
|
||||
dict(bundled=settings_loader,
|
||||
file=file_system_loader))
|
||||
return RelEnvironment(loader=select_loader, extensions=[SnippetExtension])
|
||||
|
||||
def _get_script_template(self, script_type, name, source=False):
|
||||
|
|
@ -1100,7 +1113,7 @@ class Settings(object):
|
|||
def saveScript(self, script_type, name, script):
|
||||
script_folder = self.getBaseFolder("scripts")
|
||||
filename = os.path.realpath(os.path.join(script_folder, script_type, name))
|
||||
if not filename.startswith(script_folder):
|
||||
if not filename.startswith(os.path.realpath(script_folder)):
|
||||
# oops, jail break, that shouldn't happen
|
||||
raise ValueError("Invalid script path to save to: {filename} (from {script_type}:{name})".format(**locals()))
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms
|
|||
import os
|
||||
import octoprint.plugin
|
||||
import octoprint.events
|
||||
import octoprint.util
|
||||
from octoprint.settings import settings
|
||||
|
||||
import logging
|
||||
|
|
@ -478,7 +479,7 @@ class SlicingManager(object):
|
|||
profiles = dict()
|
||||
slicer_profile_path = self.get_slicer_profile_path(slicer)
|
||||
for entry in os.listdir(slicer_profile_path):
|
||||
if not entry.endswith(".profile") or entry.startswith("."):
|
||||
if not entry.endswith(".profile") or octoprint.util.is_hidden_path(entry):
|
||||
# we are only interested in profiles and no hidden files
|
||||
continue
|
||||
|
||||
|
|
@ -539,7 +540,7 @@ class SlicingManager(object):
|
|||
name = self._sanitize(name)
|
||||
|
||||
path = os.path.join(self.get_slicer_profile_path(slicer), "{name}.profile".format(name=name))
|
||||
if not os.path.realpath(path).startswith(self._profile_path):
|
||||
if not os.path.realpath(path).startswith(os.path.realpath(self._profile_path)):
|
||||
raise IOError("Path to profile {name} tried to break out of allows sub path".format(**locals()))
|
||||
if must_exist and not (os.path.exists(path) and os.path.isfile(path)):
|
||||
raise UnknownProfile(slicer, name)
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -379,7 +379,7 @@ GCODE.renderer = (function(){
|
|||
};
|
||||
|
||||
var drawLayer = function(layerNum, fromProgress, toProgress, isNotCurrentLayer){
|
||||
console.log("Drawing layer " + layerNum + " from " + fromProgress + " to " + toProgress + " (current: " + !isNotCurrentLayer + ")");
|
||||
log.trace("Drawing layer " + layerNum + " from " + fromProgress + " to " + toProgress + " (current: " + !isNotCurrentLayer + ")");
|
||||
|
||||
var i;
|
||||
|
||||
|
|
|
|||
222
src/octoprint/static/intermediary.html
Normal file
222
src/octoprint/static/intermediary.html
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>OctoPrint is still starting</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #333333;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
#startup-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 10000;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.background {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.wrapper .outer {
|
||||
display: table;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.wrapper .outer .inner {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.wrapper .outer .inner .content {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.green {
|
||||
color: #169300;
|
||||
}
|
||||
|
||||
.red {
|
||||
color: #990000;
|
||||
}
|
||||
|
||||
.pulsate3 {
|
||||
-webkit-animation: pulsate 3s ease-out;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.pulsate1 {
|
||||
-webkit-animation: pulsate 1s ease-out;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
@-webkit-keyframes pulsate {
|
||||
0% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
50% {
|
||||
opacity: 1.0;
|
||||
}
|
||||
100% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
if (!String.prototype.endsWith) {
|
||||
String.prototype.endsWith = function(searchString, position) {
|
||||
var subjectString = this.toString();
|
||||
if (typeof position !== 'number' || !isFinite(position) || Math.floor(position) !== position || position > subjectString.length) {
|
||||
position = subjectString.length;
|
||||
}
|
||||
position -= searchString.length;
|
||||
var lastIndex = subjectString.indexOf(searchString, position);
|
||||
return lastIndex !== -1 && lastIndex === position;
|
||||
};
|
||||
}
|
||||
|
||||
function ping(url, timeout, callback) {
|
||||
var img = new Image();
|
||||
var calledBack = false;
|
||||
|
||||
var urlToUse = url;
|
||||
var postfix = "_=" + Date.now();
|
||||
if (url.indexOf("?") > -1) {
|
||||
urlToUse += "&" + postfix;
|
||||
} else {
|
||||
urlToUse += "?" + postfix;
|
||||
}
|
||||
|
||||
console.log("Pinging " + url);
|
||||
|
||||
img.onload = function() {
|
||||
callback("load");
|
||||
calledBack = true;
|
||||
};
|
||||
img.onerror = function() {
|
||||
if (!calledBack) {
|
||||
callback("error");
|
||||
calledBack = true;
|
||||
}
|
||||
};
|
||||
|
||||
img.src = urlToUse;
|
||||
setTimeout(function() {
|
||||
if (!calledBack) {
|
||||
callback("timeout");
|
||||
calledBack = true;
|
||||
}
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
window.onload = function() {
|
||||
var intervals = [1, 1, 2, 3, 5, 8];
|
||||
var timeout = 1500;
|
||||
|
||||
var baseUrl = window.location.href;
|
||||
if (baseUrl.indexOf("#") > -1) {
|
||||
baseUrl = baseUrl.substring(0, baseUrl.indexOf("#"));
|
||||
}
|
||||
if (baseUrl.indexOf("/static") > -1) {
|
||||
baseUrl = baseUrl.substring(0, baseUrl.indexOf("/static"));
|
||||
}
|
||||
if (baseUrl[baseUrl.length - 1] != "/") {
|
||||
baseUrl += "/";
|
||||
}
|
||||
var serverOnlineUrl = baseUrl + "online.gif";
|
||||
var backendOnlineUrl = baseUrl + "intermediary.gif";
|
||||
|
||||
var serverTimeout;
|
||||
|
||||
var message = window.document.getElementById("message");
|
||||
|
||||
var serverIsOnline = false;
|
||||
var serverOnlineCallback = function(result) {
|
||||
if (result == "load") {
|
||||
// our online.gif loaded, so the server is up, let's reload
|
||||
serverIsOnline = true;
|
||||
message.className = "pulsate1 green";
|
||||
message.innerText = "OctoPrint server online, reloading page...";
|
||||
window.location = baseUrl;
|
||||
} else {
|
||||
// online.gif still not available, let's look at
|
||||
var interval = 15;
|
||||
if (intervals.length) {
|
||||
interval = intervals.shift();
|
||||
}
|
||||
serverTimeout = setTimeout(function() {
|
||||
ping(serverOnlineUrl, timeout, serverOnlineCallback);
|
||||
}, interval * 1000)
|
||||
}
|
||||
};
|
||||
|
||||
var backendOfflineCounter = 0;
|
||||
var backendOnlineCallback = function(result) {
|
||||
if (serverIsOnline) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (result == "load") {
|
||||
setTimeout(function() {
|
||||
ping(backendOnlineUrl, timeout, backendOnlineCallback);
|
||||
}, 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
backendOfflineCounter++;
|
||||
if (backendOfflineCounter > 10) {
|
||||
if (serverTimeout) {
|
||||
window.clearTimeout(serverTimeout);
|
||||
}
|
||||
|
||||
message.className = "red";
|
||||
message.innerHTML = "Looks like something went wrong during startup, the server is gone again. You should check <code>octoprint.log</code>.";
|
||||
} else {
|
||||
setTimeout(function() {
|
||||
ping(backendOnlineUrl, timeout, backendOnlineCallback);
|
||||
}, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
ping(backendOnlineUrl, timeout, backendOnlineCallback);
|
||||
ping(serverOnlineUrl, timeout, serverOnlineCallback);
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="startup-overlay">
|
||||
<div class="background"></div>
|
||||
<div class="wrapper">
|
||||
<div class="outer">
|
||||
<div class="inner">
|
||||
<div class="content">
|
||||
<h1 id="message" class="pulsate3">OctoPrint is still starting up, please wait...</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -11,9 +11,6 @@ function DataUpdater(allViewModels) {
|
|||
|
||||
self._pluginHash = undefined;
|
||||
|
||||
self.reloadOverlay = $("#reloadui_overlay");
|
||||
$("#reloadui_overlay_reload").click(function() { location.reload(true); });
|
||||
|
||||
self.connect = function() {
|
||||
var options = {};
|
||||
if (SOCKJS_DEBUG) {
|
||||
|
|
@ -142,7 +139,7 @@ function DataUpdater(allViewModels) {
|
|||
}
|
||||
|
||||
if (oldVersion != VERSION || (oldPluginHash != undefined && oldPluginHash != self._pluginHash)) {
|
||||
self.reloadOverlay.show();
|
||||
showReloadOverlay();
|
||||
}
|
||||
|
||||
break;
|
||||
|
|
@ -250,7 +247,6 @@ function DataUpdater(allViewModels) {
|
|||
text: _.sprintf(gettext("Streamed %(local)s to %(remote)s on SD, took %(time).2f seconds"), payload),
|
||||
type: "success"
|
||||
});
|
||||
gcodeFilesViewModel.requestData(payload.remote, "sdcard");
|
||||
}
|
||||
|
||||
var legacyEventHandlers = {
|
||||
|
|
|
|||
|
|
@ -476,6 +476,10 @@ function showConfirmationDialog(message, onacknowledge) {
|
|||
confirmationDialog.modal("show");
|
||||
}
|
||||
|
||||
function showReloadOverlay() {
|
||||
$("#reloadui_overlay").show();
|
||||
}
|
||||
|
||||
function commentableLinesToArray(lines) {
|
||||
return splitTextToArray(lines, "\n", true, function(item) {return !_.startsWith(item, "#")});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,72 @@ $(function() {
|
|||
|
||||
log.setLevel(CONFIG_DEBUG ? "debug" : "info");
|
||||
|
||||
//~~ setup browser and internal tab tracking (in 1.3.0 that will be
|
||||
// much nicer with the global OctoPrint object...)
|
||||
|
||||
var tabTracking = (function() {
|
||||
var exports = {
|
||||
browserTabVisibility: undefined,
|
||||
selectedTab: undefined
|
||||
};
|
||||
|
||||
var browserVisibilityCallbacks = [];
|
||||
|
||||
var getHiddenProp = function() {
|
||||
var prefixes = ["webkit", "moz", "ms", "o"];
|
||||
|
||||
// if "hidden" is natively supported just return it
|
||||
if ("hidden" in document) {
|
||||
return "hidden"
|
||||
}
|
||||
|
||||
// otherwise loop over all the known prefixes until we find one
|
||||
var vendorPrefix = _.find(prefixes, function(prefix) {
|
||||
return (prefix + "Hidden" in document);
|
||||
});
|
||||
if (vendorPrefix !== undefined) {
|
||||
return vendorPrefix + "Hidden";
|
||||
}
|
||||
|
||||
// nothing found
|
||||
return undefined;
|
||||
};
|
||||
|
||||
var isHidden = function() {
|
||||
var prop = getHiddenProp();
|
||||
if (!prop) return false;
|
||||
|
||||
return document[prop];
|
||||
};
|
||||
|
||||
var updateBrowserVisibility = function() {
|
||||
var visible = !isHidden();
|
||||
exports.browserTabVisible = visible;
|
||||
_.each(browserVisibilityCallbacks, function(callback) {
|
||||
callback(visible);
|
||||
})
|
||||
};
|
||||
|
||||
// register for browser visibility tracking
|
||||
|
||||
var prop = getHiddenProp();
|
||||
if (!prop) return undefined;
|
||||
|
||||
var eventName = prop.replace(/[H|h]idden/, "") + "visibilitychange";
|
||||
document.addEventListener(eventName, updateBrowserVisibility);
|
||||
|
||||
updateBrowserVisibility();
|
||||
|
||||
// exports
|
||||
|
||||
exports.isVisible = function() { return !isHidden() };
|
||||
exports.onBrowserVisibilityChange = function(callback) {
|
||||
browserVisibilityCallbacks.push(callback);
|
||||
};
|
||||
|
||||
return exports;
|
||||
})();
|
||||
|
||||
//~~ AJAX setup
|
||||
|
||||
// work around a stupid iOS6 bug where ajax requests get cached and only work once, as described at
|
||||
|
|
@ -67,6 +133,18 @@ $(function() {
|
|||
// the view model map is our basic look up table for dependencies that may be injected into other view models
|
||||
var viewModelMap = {};
|
||||
|
||||
// We put our tabTracking into the viewModelMap as a workaround until
|
||||
// our global OctoPrint object becomes available in 1.3.0. This way
|
||||
// we'll still be able to access it in our view models.
|
||||
//
|
||||
// NOTE TO DEVELOPERS: Do NOT depend on this dependency in your custom
|
||||
// view models. It is ONLY provided for the core application to be able
|
||||
// to backport a fix from the 1.3.0 development branch and WILL BE
|
||||
// REMOVED once 1.3.0 gets released without any fallback!
|
||||
//
|
||||
// TODO: Remove with release of 1.3.0
|
||||
viewModelMap.tabTracking = tabTracking;
|
||||
|
||||
// Fix Function#name on browsers that do not support it (IE):
|
||||
// see: http://stackoverflow.com/questions/6903762/function-name-not-supported-in-ie
|
||||
if (!(function f() {}).name) {
|
||||
|
|
@ -371,16 +449,22 @@ $(function() {
|
|||
$('.nav-pills, .nav-tabs').tabdrop();
|
||||
|
||||
// Allow components to react to tab change
|
||||
var tabs = $('#tabs a[data-toggle="tab"]');
|
||||
tabs.on('show', function (e) {
|
||||
var current = e.target.hash;
|
||||
var previous = e.relatedTarget.hash;
|
||||
var onTabChange = function(current, previous) {
|
||||
log.debug("Selected OctoPrint tab changed: previous = " + previous + ", current = " + current);
|
||||
tabTracking.selectedTab = current;
|
||||
|
||||
_.each(allViewModels, function(viewModel) {
|
||||
if (viewModel.hasOwnProperty("onTabChange")) {
|
||||
viewModel.onTabChange(current, previous);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var tabs = $('#tabs a[data-toggle="tab"]');
|
||||
tabs.on('show', function (e) {
|
||||
var current = e.target.hash;
|
||||
var previous = e.relatedTarget.hash;
|
||||
onTabChange(current, previous);
|
||||
});
|
||||
|
||||
tabs.on('shown', function (e) {
|
||||
|
|
@ -394,6 +478,8 @@ $(function() {
|
|||
});
|
||||
});
|
||||
|
||||
onTabChange(OCTOPRINT_INITIAL_TAB);
|
||||
|
||||
// Fix input element click problems on dropdowns
|
||||
$(".dropdown input, .dropdown label").click(function(e) {
|
||||
e.stopPropagation();
|
||||
|
|
@ -404,6 +490,9 @@ $(function() {
|
|||
e.preventDefault();
|
||||
});
|
||||
|
||||
// reload overlay
|
||||
$("#reloadui_overlay_reload").click(function() { location.reload(); });
|
||||
|
||||
//~~ Starting up the app
|
||||
|
||||
_.each(allViewModels, function(viewModel) {
|
||||
|
|
@ -485,11 +574,22 @@ $(function() {
|
|||
});
|
||||
log.info("... binding done");
|
||||
|
||||
// startup complete
|
||||
_.each(allViewModels, function(viewModel) {
|
||||
if (viewModel.hasOwnProperty("onStartupComplete")) {
|
||||
viewModel.onStartupComplete();
|
||||
}
|
||||
});
|
||||
|
||||
// make sure we can track the browser tab visibility
|
||||
tabTracking.onBrowserVisibilityChange(function(status) {
|
||||
log.debug("Browser tab is now " + (status ? "visible" : "hidden"));
|
||||
_.each(allViewModels, function(viewModel) {
|
||||
if (viewModel.hasOwnProperty("onBrowserTabVisibilityChange")) {
|
||||
viewModel.onBrowserTabVisibilityChange(status);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
if (!_.has(viewModelMap, "settingsViewModel")) {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ $(function() {
|
|||
self.loginState = parameters[0];
|
||||
self.settings = parameters[1];
|
||||
|
||||
// TODO remove with release of 1.3.0 and switch to OctoPrint.coreui usage
|
||||
self.tabTracking = parameters[2];
|
||||
|
||||
self._createToolEntry = function () {
|
||||
return {
|
||||
name: ko.observable(),
|
||||
|
|
@ -415,31 +418,51 @@ $(function() {
|
|||
|
||||
self.onSettingsBeforeSave = self.updateRotatorWidth;
|
||||
|
||||
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
|
||||
self.webcamDisableTimeout = setTimeout(function () {
|
||||
$("#webcam_image").attr("src", "");
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
self._enableWebcam = function() {
|
||||
if (self.tabTracking.selectedTab != "#control" || !self.tabTracking.browserTabVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.webcamDisableTimeout != undefined) {
|
||||
clearTimeout(self.webcamDisableTimeout);
|
||||
}
|
||||
var webcamImage = $("#webcam_image");
|
||||
var currentSrc = webcamImage.attr("src");
|
||||
if (currentSrc === undefined || currentSrc.trim() == "") {
|
||||
var newSrc = CONFIG_WEBCAM_STREAM;
|
||||
if (CONFIG_WEBCAM_STREAM.lastIndexOf("?") > -1) {
|
||||
newSrc += "&";
|
||||
} else {
|
||||
newSrc += "?";
|
||||
}
|
||||
newSrc += new Date().getTime();
|
||||
|
||||
self.updateRotatorWidth();
|
||||
webcamImage.attr("src", newSrc);
|
||||
}
|
||||
};
|
||||
|
||||
self.onTabChange = function (current, previous) {
|
||||
if (current == "#control") {
|
||||
if (self.webcamDisableTimeout != undefined) {
|
||||
clearTimeout(self.webcamDisableTimeout);
|
||||
}
|
||||
var webcamImage = $("#webcam_image");
|
||||
var currentSrc = webcamImage.attr("src");
|
||||
if (currentSrc === undefined || currentSrc.trim() == "") {
|
||||
var newSrc = CONFIG_WEBCAM_STREAM;
|
||||
if (CONFIG_WEBCAM_STREAM.lastIndexOf("?") > -1) {
|
||||
newSrc += "&";
|
||||
} else {
|
||||
newSrc += "?";
|
||||
}
|
||||
newSrc += new Date().getTime();
|
||||
|
||||
self.updateRotatorWidth();
|
||||
webcamImage.attr("src", newSrc);
|
||||
}
|
||||
self._enableWebcam();
|
||||
} else if (previous == "#control") {
|
||||
// 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
|
||||
self.webcamDisableTimeout = setTimeout(function () {
|
||||
$("#webcam_image").attr("src", "");
|
||||
}, 5000);
|
||||
self._disableWebcam();
|
||||
}
|
||||
};
|
||||
|
||||
self.onBrowserTabVisibilityChange = function(status) {
|
||||
if (status) {
|
||||
self._enableWebcam();
|
||||
} else {
|
||||
self._disableWebcam();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -565,7 +588,7 @@ $(function() {
|
|||
|
||||
OCTOPRINT_VIEWMODELS.push([
|
||||
ControlViewModel,
|
||||
["loginStateViewModel", "settingsViewModel"],
|
||||
["loginStateViewModel", "settingsViewModel", "tabTracking"],
|
||||
"#control"
|
||||
]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -600,6 +600,10 @@ $(function() {
|
|||
self.onEventMetadataStatisticsUpdated = function(payload) {
|
||||
self.requestData();
|
||||
};
|
||||
|
||||
self.onEventTransferDone = function(payload) {
|
||||
self.requestData(payload.remote, "sdcard");
|
||||
};
|
||||
}
|
||||
|
||||
OCTOPRINT_VIEWMODELS.push([
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ $(function() {
|
|||
};
|
||||
self._sendData(data, function() {
|
||||
// if the user indeed disables access control, we'll need to reload the page for this to take effect
|
||||
//location.reload(true); // TODO: clear cache doesn't work properly, needs a better way, same issue with reloading plugins
|
||||
showReloadOverlay();
|
||||
});
|
||||
});
|
||||
$("#confirmation_dialog").modal("show");
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ $(function() {
|
|||
self.loginState = parameters[0];
|
||||
self.settings = parameters[1];
|
||||
|
||||
// TODO remove with release of 1.3.0 and switch to OctoPrint.coreui usage
|
||||
self.tabTracking = parameters[2];
|
||||
|
||||
self.ui_progress_percentage = ko.observable();
|
||||
self.ui_progress_type = ko.observable();
|
||||
self.ui_progress_text = ko.computed(function() {
|
||||
|
|
@ -339,6 +342,21 @@ $(function() {
|
|||
self._processData(data);
|
||||
};
|
||||
|
||||
self._renderPercentage = function(percentage) {
|
||||
var cmdIndex = GCODE.gCodeReader.getCmdIndexForPercentage(percentage);
|
||||
if (!cmdIndex) return;
|
||||
|
||||
GCODE.renderer.render(cmdIndex.layer, 0, cmdIndex.cmd);
|
||||
GCODE.ui.updateLayerInfo(cmdIndex.layer);
|
||||
|
||||
if (self.layerSlider != undefined) {
|
||||
self.layerSlider.slider("setValue", cmdIndex.layer);
|
||||
}
|
||||
if (self.layerCommandSlider != undefined) {
|
||||
self.layerCommandSlider.slider("setValue", [0, cmdIndex.cmd]);
|
||||
}
|
||||
};
|
||||
|
||||
self._processData = function(data) {
|
||||
if (!data.job.file || !data.job.file.name && (self.loadedFilename || self.loadedFileDate)) {
|
||||
self.waitForApproval(false);
|
||||
|
|
@ -358,19 +376,8 @@ $(function() {
|
|||
if(self.loadedFilename
|
||||
&& self.loadedFilename == data.job.file.name
|
||||
&& self.loadedFileDate == data.job.file.date) {
|
||||
if (self.currentlyPrinting && self.renderer_syncProgress() && !self.waitForApproval()) {
|
||||
var cmdIndex = GCODE.gCodeReader.getCmdIndexForPercentage(data.progress.completion);
|
||||
if(cmdIndex){
|
||||
GCODE.renderer.render(cmdIndex.layer, 0, cmdIndex.cmd);
|
||||
GCODE.ui.updateLayerInfo(cmdIndex.layer);
|
||||
|
||||
if (self.layerSlider != undefined) {
|
||||
self.layerSlider.slider("setValue", cmdIndex.layer);
|
||||
}
|
||||
if (self.layerCommandSlider != undefined) {
|
||||
self.layerCommandSlider.slider("setValue", [0, cmdIndex.cmd]);
|
||||
}
|
||||
}
|
||||
if (self.tabTracking.browserTabVisible && self.tabActive && self.currentlyPrinting && self.renderer_syncProgress() && !self.waitForApproval()) {
|
||||
self._renderPercentage(data.progress.completion);
|
||||
}
|
||||
self.errorCount = 0
|
||||
} else {
|
||||
|
|
@ -394,6 +401,12 @@ $(function() {
|
|||
}
|
||||
};
|
||||
|
||||
self.onEventPrintDone = function() {
|
||||
if (self.renderer_syncProgress() && !self.waitForApproval()) {
|
||||
self._renderPercentage(100.0);
|
||||
}
|
||||
};
|
||||
|
||||
self.approveLargeFile = function() {
|
||||
self.waitForApproval(false);
|
||||
self.loadFile(self.selectedFile.name(), self.selectedFile.date());
|
||||
|
|
@ -504,13 +517,16 @@ $(function() {
|
|||
|
||||
self.onBeforeBinding = function() {
|
||||
self.initialize();
|
||||
}
|
||||
};
|
||||
|
||||
self.onTabChange = function(current, previous) {
|
||||
self.tabActive = current == "#gcode";
|
||||
};
|
||||
}
|
||||
|
||||
OCTOPRINT_VIEWMODELS.push([
|
||||
GcodeViewModel,
|
||||
["loginStateViewModel", "settingsViewModel"],
|
||||
["loginStateViewModel", "settingsViewModel", "tabTracking"],
|
||||
"#gcode"
|
||||
]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ $(function() {
|
|||
function LoginStateViewModel() {
|
||||
var self = this;
|
||||
|
||||
self.loginUser = ko.observable();
|
||||
self.loginPass = ko.observable();
|
||||
self.loginUser = ko.observable("");
|
||||
self.loginPass = ko.observable("");
|
||||
self.loginRemember = ko.observable(false);
|
||||
|
||||
self.loggedIn = ko.observable(false);
|
||||
|
|
@ -79,10 +79,6 @@ $(function() {
|
|||
var password = self.loginPass();
|
||||
var remember = self.loginRemember();
|
||||
|
||||
self.loginUser("");
|
||||
self.loginPass("");
|
||||
self.loginRemember(false);
|
||||
|
||||
$.ajax({
|
||||
url: API_BASEURL + "login",
|
||||
type: "POST",
|
||||
|
|
@ -90,6 +86,10 @@ $(function() {
|
|||
success: function(response) {
|
||||
new PNotify({title: gettext("Login successful"), text: _.sprintf(gettext('You are now logged in as "%(username)s"'), {username: response.name}), type: "success"});
|
||||
self.fromResponse(response);
|
||||
|
||||
self.loginUser("");
|
||||
self.loginPass("");
|
||||
self.loginRemember(false);
|
||||
},
|
||||
error: function(jqXHR, textStatus, errorThrown) {
|
||||
new PNotify({title: gettext("Login failed"), text: gettext("User unknown or wrong password"), type: "error"});
|
||||
|
|
@ -138,4 +138,4 @@ $(function() {
|
|||
[],
|
||||
[]
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ $(function() {
|
|||
height: 200,
|
||||
origin: "lowerleft"
|
||||
},
|
||||
heatedBed: false,
|
||||
heatedBed: true,
|
||||
axes: {
|
||||
x: {speed: 6000, inverted: false},
|
||||
y: {speed: 6000, inverted: false},
|
||||
|
|
|
|||
|
|
@ -250,9 +250,20 @@ $(function() {
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
// reset scroll position on tab change
|
||||
$('ul.nav-list a[data-toggle="tab"]', self.settingsDialog).on("show", function() {
|
||||
self._resetScrollPosition();
|
||||
});
|
||||
};
|
||||
|
||||
self.show = function() {
|
||||
self.show = function(tab) {
|
||||
// select first or specified tab
|
||||
self.selectTab(tab);
|
||||
|
||||
// reset scroll position
|
||||
self._resetScrollPosition();
|
||||
|
||||
// show settings, ensure centered position
|
||||
self.settingsDialog.modal({
|
||||
minHeight: function() { return Math.max($.fn.modal.defaults.maxHeight() - 80, 250); }
|
||||
|
|
@ -587,6 +598,21 @@ $(function() {
|
|||
self.onEventSettingsUpdated = function() {
|
||||
self.requestData();
|
||||
};
|
||||
|
||||
self._resetScrollPosition = function() {
|
||||
$('.scrollable', self.settingsDialog).scrollTop(0);
|
||||
};
|
||||
|
||||
self.selectTab = function(tab) {
|
||||
if (tab != undefined) {
|
||||
if (!_.startsWith(tab, "#")) {
|
||||
tab = "#" + tab;
|
||||
}
|
||||
$('ul.nav-list a[href="' + tab + '"]', self.settingsDialog).tab("show");
|
||||
} else {
|
||||
$('ul.nav-list a:first', self.settingsDialog).tab("show");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
OCTOPRINT_VIEWMODELS.push([
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ $(function() {
|
|||
|
||||
self.heaterOptions = ko.observable({});
|
||||
|
||||
self._numExtrudersUpdated = function() {
|
||||
self._printerProfileUpdated = function() {
|
||||
var graphColors = ["red", "orange", "green", "brown", "purple"];
|
||||
var heaterOptions = {};
|
||||
var tools = self.tools();
|
||||
|
|
@ -69,15 +69,21 @@ $(function() {
|
|||
}
|
||||
|
||||
// print bed
|
||||
heaterOptions["bed"] = {name: gettext("Bed"), color: "blue"};
|
||||
if (self.settingsViewModel.printerProfiles.currentProfileData().heatedBed()) {
|
||||
self.hasBed(true);
|
||||
heaterOptions["bed"] = {name: gettext("Bed"), color: "blue"};
|
||||
} else {
|
||||
self.hasBed(false);
|
||||
}
|
||||
|
||||
// write back
|
||||
self.heaterOptions(heaterOptions);
|
||||
self.tools(tools);
|
||||
};
|
||||
self.settingsViewModel.printerProfiles.currentProfileData.subscribe(function() {
|
||||
self._numExtrudersUpdated();
|
||||
self.settingsViewModel.printerProfiles.currentProfileData().extruder.count.subscribe(self._numExtrudersUpdated);
|
||||
self._printerProfileUpdated();
|
||||
self.settingsViewModel.printerProfiles.currentProfileData().extruder.count.subscribe(self._printerProfileUpdated);
|
||||
self.settingsViewModel.printerProfiles.currentProfileData().heatedBed.subscribe(self._printerProfileUpdated());
|
||||
});
|
||||
|
||||
self.temperatures = [];
|
||||
|
|
@ -152,11 +158,8 @@ $(function() {
|
|||
}
|
||||
|
||||
if (lastData.hasOwnProperty("bed")) {
|
||||
self.hasBed(true);
|
||||
self.bedTemp["actual"](lastData.bed.actual);
|
||||
self.bedTemp["target"](lastData.bed.target);
|
||||
} else {
|
||||
self.hasBed(false);
|
||||
}
|
||||
|
||||
if (!CONFIG_TEMPERATURE_GRAPH) return;
|
||||
|
|
@ -208,8 +211,6 @@ $(function() {
|
|||
if (!d[type]) return;
|
||||
result[type].actual.push([time, d[type].actual]);
|
||||
result[type].target.push([time, d[type].target]);
|
||||
|
||||
self.hasBed(self.hasBed() || (type == "bed"));
|
||||
})
|
||||
});
|
||||
|
||||
|
|
@ -375,4 +376,4 @@ $(function() {
|
|||
["loginStateViewModel", "settingsViewModel"],
|
||||
"#temp"
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ $(function() {
|
|||
|
||||
self.log = ko.observableArray([]);
|
||||
self.buffer = ko.observable(300);
|
||||
self.upperLimit = ko.observable(3000);
|
||||
|
||||
self.command = ko.observable(undefined);
|
||||
|
||||
|
|
@ -34,7 +35,8 @@ $(function() {
|
|||
|
||||
var filtered = false;
|
||||
var result = [];
|
||||
_.each(self.log(), function(entry) {
|
||||
var lines = self.log();
|
||||
_.each(lines, function(entry) {
|
||||
if (lineVisible(entry)) {
|
||||
result.push(entry);
|
||||
filtered = false;
|
||||
|
|
@ -46,19 +48,30 @@ $(function() {
|
|||
|
||||
return result;
|
||||
});
|
||||
self.displayedLines.subscribe(function() {
|
||||
self.updateOutput();
|
||||
});
|
||||
|
||||
self.lineCount = ko.computed(function() {
|
||||
var total = self.log().length;
|
||||
var displayed = _.filter(self.displayedLines(), function(entry) { return entry.type == "line" }).length;
|
||||
var regex = self.filterRegex();
|
||||
var lineVisible = function(entry) {
|
||||
return regex == undefined || !entry.line.match(regex);
|
||||
};
|
||||
|
||||
var lines = self.log();
|
||||
var total = lines.length;
|
||||
var displayed = _.filter(lines, lineVisible).length;
|
||||
var filtered = total - displayed;
|
||||
|
||||
if (total == displayed) {
|
||||
return _.sprintf(gettext("showing %(displayed)d lines"), {displayed: displayed});
|
||||
if (filtered > 0) {
|
||||
if (total > self.upperLimit()) {
|
||||
return _.sprintf(gettext("showing %(displayed)d lines (%(filtered)d of %(total)d total lines filtered, buffer full)"), {displayed: displayed, total: total, filtered: filtered});
|
||||
} else {
|
||||
return _.sprintf(gettext("showing %(displayed)d lines (%(filtered)d of %(total)d total lines filtered)"), {displayed: displayed, total: total, filtered: filtered});
|
||||
}
|
||||
} else {
|
||||
return _.sprintf(gettext("showing %(displayed)d lines (%(filtered)d of %(total)d total lines filtered)"), {displayed: displayed, total: total, filtered: filtered});
|
||||
if (total > self.upperLimit()) {
|
||||
return _.sprintf(gettext("showing %(displayed)d lines (buffer full)"), {displayed: displayed});
|
||||
} else {
|
||||
return _.sprintf(gettext("showing %(displayed)d lines"), {displayed: displayed});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -84,14 +97,31 @@ $(function() {
|
|||
};
|
||||
|
||||
self._processCurrentLogData = function(data) {
|
||||
self.log(self.log().concat(_.map(data, function(line) { return self._toInternalFormat(line) })));
|
||||
if (self.autoscrollEnabled()) {
|
||||
self.log(self.log.slice(-self.buffer()));
|
||||
var length = self.log().length;
|
||||
if (length >= self.upperLimit()) {
|
||||
var cutoff = "--- too many lines to buffer, cut off ---";
|
||||
var last = self.log()[length-1];
|
||||
if (!last || last.type != "cut" || last.line != cutoff) {
|
||||
self.log(self.log().concat(self._toInternalFormat(cutoff, "cut")));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var newLog = self.log().concat(_.map(data, function(line) { return self._toInternalFormat(line) }));
|
||||
if (self.autoscrollEnabled()) {
|
||||
// we only keep the last <buffer> entries
|
||||
newLog = newLog.slice(-self.buffer());
|
||||
} else if (newLog.length > self.upperLimit()) {
|
||||
// we only keep the first <upperLimit> entries
|
||||
newLog = newLog.slice(0, self.upperLimit());
|
||||
}
|
||||
self.log(newLog);
|
||||
self.updateOutput();
|
||||
};
|
||||
|
||||
self._processHistoryLogData = function(data) {
|
||||
self.log(_.map(data, function(line) { return self._toInternalFormat(line) }));
|
||||
self.updateOutput();
|
||||
};
|
||||
|
||||
self._toInternalFormat = function(line, type) {
|
||||
|
|
@ -141,7 +171,7 @@ $(function() {
|
|||
self.scrollToEnd = function() {
|
||||
var container = $("#terminal-output");
|
||||
if (container.length) {
|
||||
container.scrollTop(container[0].scrollHeight - container.height())
|
||||
container.scrollTop(container[0].scrollHeight);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -232,4 +262,4 @@ $(function() {
|
|||
["loginStateViewModel", "settingsViewModel"],
|
||||
"#term"
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -396,10 +396,17 @@ ul.dropdown-menu li a {
|
|||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 10001;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#offline_overlay {
|
||||
z-index: 10002;
|
||||
}
|
||||
|
||||
#reloadui_overlay {
|
||||
z-index: 10001;
|
||||
}
|
||||
|
||||
#offline_overlay_background,
|
||||
#reloadui_overlay_background {
|
||||
position: fixed;
|
||||
|
|
|
|||
|
|
@ -33,4 +33,12 @@
|
|||
var OCTOPRINT_VIEWMODELS = [];
|
||||
var ADDITIONAL_VIEWMODELS = [];
|
||||
var OCTOPRINT_ADDITIONAL_BINDINGS = [];
|
||||
|
||||
{% if templates.tab.order %}
|
||||
{% set first_tab = templates.tab.order[0] %}
|
||||
{% set entry, data = templates.tab.entries[first_tab] %}
|
||||
var OCTOPRINT_INITIAL_TAB = "#{{ data._div }}";
|
||||
{% else %}
|
||||
var OCTOPRINT_INITIAL_TAB = undefined;
|
||||
{% endif %}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
<a id="navbar_show_settings" class="pull-right" href="#settings_dialog" data-bind="click: show">
|
||||
<a id="navbar_show_settings" class="pull-right" href="#settings_dialog" data-bind="click: function() { $root.show(); }">
|
||||
<i class="icon-wrench"></i> {{ _('Settings') }}
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" data-bind="visible: systemActions().length > 0">
|
||||
<i class="icon-off"></i> {{ _('System') }}
|
||||
<b class="caret"></b>
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<div class="settings-trigger accordion-heading-button btn-group">
|
||||
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
|
||||
<a class="dropdown-toggle" data-toggle="dropdown" href="#" title="{{ _('File list settings') }}">
|
||||
<span class="icon-wrench"></span>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
|
|
@ -19,9 +19,15 @@
|
|||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="refresh-trigger accordion-heading-button btn-group">
|
||||
<a href="#" data-bind="click: function() { $root.requestData(); }" title="{{ _('Refresh file list') }}">
|
||||
<span class="icon-refresh"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% if enableSdSupport %}
|
||||
<div class="sd-trigger accordion-heading-button btn-group">
|
||||
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
|
||||
<a class="dropdown-toggle" data-toggle="dropdown" href="#" title="{{ _('SD Card operations') }}">
|
||||
<span class="icon-sd-black-14"></span>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<div class="terminal">
|
||||
<pre id="terminal-output" class="pre-scrollable" data-bind="foreach: displayedLines"><span data-bind="text: line, css: {muted: type == 'filtered'}"></span><br></pre>
|
||||
<pre id="terminal-output" class="pre-scrollable" data-bind="foreach: displayedLines"><span data-bind="text: line, css: {muted: type == 'filtered' || type == 'cut'}"></span><br></pre>
|
||||
<small class="pull-left"><button class="btn btn-mini" data-bind="click: toggleAutoscroll, css: {active: autoscrollEnabled}">{{ _('Autoscroll') }}</button> <span data-bind="text: lineCount"></span></small>
|
||||
<small class="pull-right"><a href="#" data-bind="click: scrollToEnd">{{ _("Scroll to end") }}</a> | <a href="#" data-bind="click: selectAll">{{ _("Select all") }}</a></small>
|
||||
</div>
|
||||
|
|
|
|||
Binary file not shown.
File diff suppressed because it is too large
Load diff
|
|
@ -26,6 +26,21 @@ class UserManager(object):
|
|||
self._logger = logging.getLogger(__name__)
|
||||
self._session_users_by_session = dict()
|
||||
self._session_users_by_userid = dict()
|
||||
self._enabled = True
|
||||
|
||||
@property
|
||||
def enabled(self):
|
||||
return self._enabled
|
||||
|
||||
@enabled.setter
|
||||
def enabled(self, value):
|
||||
self._enabled = value
|
||||
|
||||
def enable(self):
|
||||
self._enabled = True
|
||||
|
||||
def disable(self):
|
||||
self._enabled = False
|
||||
|
||||
def login_user(self, user):
|
||||
self._cleanup_sessions()
|
||||
|
|
|
|||
|
|
@ -542,6 +542,48 @@ def dict_contains_keys(keys, dictionary):
|
|||
|
||||
return True
|
||||
|
||||
|
||||
def dict_filter(dictionary, filter_function):
|
||||
"""
|
||||
Filters a dictionary with the provided filter_function
|
||||
|
||||
Example::
|
||||
|
||||
>>> data = dict(key1="value1", key2="value2", other_key="other_value", foo="bar", bar="foo")
|
||||
>>> dict_filter(data, lambda k, v: k.startswith("key")) == dict(key1="value1", key2="value2")
|
||||
True
|
||||
>>> dict_filter(data, lambda k, v: v.startswith("value")) == dict(key1="value1", key2="value2")
|
||||
True
|
||||
>>> dict_filter(data, lambda k, v: k == "foo" or v == "foo") == dict(foo="bar", bar="foo")
|
||||
True
|
||||
>>> dict_filter(data, lambda k, v: False) == dict()
|
||||
True
|
||||
>>> dict_filter(data, lambda k, v: True) == data
|
||||
True
|
||||
>>> dict_filter(None, lambda k, v: True)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
AssertionError
|
||||
>>> dict_filter(data, None)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
AssertionError
|
||||
|
||||
Arguments:
|
||||
dictionary (dict): The dictionary to filter
|
||||
filter_function (callable): The filter function to apply, called with key and
|
||||
value of an entry in the dictionary, must return ``True`` for values to
|
||||
keep and ``False`` for values to strip
|
||||
|
||||
Returns:
|
||||
dict: A shallow copy of the provided dictionary, stripped of the key-value-pairs
|
||||
for which the ``filter_function`` returned ``False``
|
||||
"""
|
||||
assert isinstance(dictionary, dict)
|
||||
assert callable(filter_function)
|
||||
return dict((k, v) for k, v in dictionary.items() if filter_function(k, v))
|
||||
|
||||
|
||||
class Object(object):
|
||||
pass
|
||||
|
||||
|
|
@ -584,8 +626,10 @@ def address_for_client(host, port):
|
|||
@contextlib.contextmanager
|
||||
def atomic_write(filename, mode="w+b", prefix="tmp", suffix=""):
|
||||
temp_config = tempfile.NamedTemporaryFile(mode=mode, prefix=prefix, suffix=suffix, delete=False)
|
||||
yield temp_config
|
||||
temp_config.close()
|
||||
try:
|
||||
yield temp_config
|
||||
finally:
|
||||
temp_config.close()
|
||||
shutil.move(temp_config.name, filename)
|
||||
|
||||
|
||||
|
|
@ -609,7 +653,32 @@ def bom_aware_open(filename, encoding="ascii", mode="r", **kwargs):
|
|||
if header.startswith(bom):
|
||||
encoding += "-sig"
|
||||
|
||||
return codecs.open(filename, encoding=encoding, **kwargs)
|
||||
return codecs.open(filename, encoding=encoding, mode=mode, **kwargs)
|
||||
|
||||
|
||||
def is_hidden_path(path):
|
||||
if path is None:
|
||||
# we define a None path as not hidden here
|
||||
return False
|
||||
|
||||
filename = os.path.basename(path)
|
||||
if filename.startswith("."):
|
||||
# filenames starting with a . are hidden
|
||||
return True
|
||||
|
||||
if sys.platform == "win32":
|
||||
# if we are running on windows we also try to read the hidden file
|
||||
# attribute via the windows api
|
||||
try:
|
||||
import ctypes
|
||||
attrs = ctypes.windll.kernel32.GetFileAttributesW(unicode(path))
|
||||
assert attrs != -1 # INVALID_FILE_ATTRIBUTES == -1
|
||||
return bool(attrs & 2) # FILE_ATTRIBUTE_HIDDEN == 2
|
||||
except (AttributeError, AssertionError):
|
||||
pass
|
||||
|
||||
# if we reach that point, the path is not hidden
|
||||
return False
|
||||
|
||||
|
||||
class RepeatedTimer(threading.Thread):
|
||||
|
|
|
|||
|
|
@ -262,6 +262,7 @@ class MachineCom(object):
|
|||
self._serial_factory_hooks = self._pluginManager.get_hooks("octoprint.comm.transport.serial.factory")
|
||||
|
||||
# SD status data
|
||||
self._sdEnabled = settings().getBoolean(["feature", "sdSupport"])
|
||||
self._sdAvailable = False
|
||||
self._sdFileList = False
|
||||
self._sdFiles = []
|
||||
|
|
@ -719,21 +720,28 @@ class MachineCom(object):
|
|||
return self._sdFiles
|
||||
|
||||
def startSdFileTransfer(self, filename):
|
||||
if not self.isOperational() or self.isBusy():
|
||||
if not self._sdEnabled:
|
||||
return
|
||||
|
||||
if not self.isOperational() or self.isBusy():
|
||||
return
|
||||
self._changeState(self.STATE_TRANSFERING_FILE)
|
||||
self.sendCommand("M28 %s" % filename.lower())
|
||||
|
||||
def endSdFileTransfer(self, filename):
|
||||
if not self.isOperational() or self.isBusy():
|
||||
if not self._sdEnabled:
|
||||
return
|
||||
|
||||
if not self.isOperational() or self.isBusy():
|
||||
return
|
||||
self.sendCommand("M29 %s" % filename.lower())
|
||||
self._changeState(self.STATE_OPERATIONAL)
|
||||
self.refreshSdFiles()
|
||||
|
||||
def deleteSdFile(self, filename):
|
||||
if not self._sdEnabled:
|
||||
return
|
||||
|
||||
if not self.isOperational() or (self.isBusy() and
|
||||
isinstance(self._currentFile, PrintingSdFileInformation) and
|
||||
self._currentFile.getFilename() == filename):
|
||||
|
|
@ -744,13 +752,21 @@ class MachineCom(object):
|
|||
self.refreshSdFiles()
|
||||
|
||||
def refreshSdFiles(self):
|
||||
if not self._sdEnabled:
|
||||
return
|
||||
|
||||
if not self.isOperational() or self.isBusy():
|
||||
return
|
||||
|
||||
self.sendCommand("M20")
|
||||
|
||||
def initSdCard(self):
|
||||
if not self._sdEnabled:
|
||||
return
|
||||
|
||||
if not self.isOperational():
|
||||
return
|
||||
|
||||
self.sendCommand("M21")
|
||||
if settings().getBoolean(["feature", "sdAlwaysAvailable"]):
|
||||
self._sdAvailable = True
|
||||
|
|
@ -758,6 +774,9 @@ class MachineCom(object):
|
|||
self._callback.on_comm_sd_state_change(self._sdAvailable)
|
||||
|
||||
def releaseSdCard(self):
|
||||
if not self._sdEnabled:
|
||||
return
|
||||
|
||||
if not self.isOperational() or (self.isBusy() and self.isSdFileSelected()):
|
||||
# do not release the sd card if we are currently printing from it
|
||||
return
|
||||
|
|
@ -1098,13 +1117,7 @@ class MachineCom(object):
|
|||
### Operational
|
||||
elif self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PAUSED:
|
||||
if "ok" in line:
|
||||
# if we still have commands to process, process them
|
||||
if self._resendSwallowNextOk:
|
||||
self._resendSwallowNextOk = False
|
||||
elif self._resendDelta is not None:
|
||||
self._resendNextCommand()
|
||||
elif self._sendFromQueue():
|
||||
pass
|
||||
self._handle_ok()
|
||||
|
||||
# resend -> start resend procedure from requested line
|
||||
elif line.lower().startswith("resend") or line.lower().startswith("rs"):
|
||||
|
|
@ -1122,18 +1135,8 @@ class MachineCom(object):
|
|||
|
||||
if "ok" in line or (supportWait and "wait" in line):
|
||||
# a wait while printing means our printer's buffer ran out, probably due to some ok getting
|
||||
# swallowed, so we treat it the same as an ok here teo take up communication again
|
||||
if self._resendSwallowNextOk:
|
||||
self._resendSwallowNextOk = False
|
||||
|
||||
elif self._resendDelta is not None:
|
||||
self._resendNextCommand()
|
||||
|
||||
else:
|
||||
if self._sendFromQueue():
|
||||
pass
|
||||
elif not self.isSdPrinting():
|
||||
self._sendNext()
|
||||
# swallowed, so we treat it the same as an ok here to take up communication again
|
||||
self._handle_ok()
|
||||
|
||||
elif line.lower().startswith("resend") or line.lower().startswith("rs"):
|
||||
self._handleResendRequest(line)
|
||||
|
|
@ -1147,6 +1150,24 @@ class MachineCom(object):
|
|||
eventManager().fire(Events.ERROR, {"error": self.getErrorString()})
|
||||
self._log("Connection closed, closing down monitor")
|
||||
|
||||
def _handle_ok(self):
|
||||
if not self._state in (self.STATE_PRINTING, self.STATE_OPERATIONAL, self.STATE_PAUSED):
|
||||
return
|
||||
|
||||
if self._resendSwallowNextOk:
|
||||
self._resendSwallowNextOk = False
|
||||
elif self._resendDelta is not None:
|
||||
self._resendNextCommand()
|
||||
else:
|
||||
self._continue_sending()
|
||||
|
||||
def _continue_sending(self):
|
||||
if self._state == self.STATE_PRINTING:
|
||||
if not self._sendFromQueue() and not self.isSdPrinting():
|
||||
self._sendNext()
|
||||
elif self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PAUSED:
|
||||
self._sendFromQueue()
|
||||
|
||||
def _process_registered_message(self, line, feedback_matcher, feedback_controls, feedback_errors):
|
||||
feedback_match = feedback_matcher.search(line)
|
||||
if feedback_match is None:
|
||||
|
|
@ -1221,20 +1242,29 @@ class MachineCom(object):
|
|||
self.sendGcodeScript("afterPrinterConnected", replacements=dict(event=payload))
|
||||
|
||||
def _sendFromQueue(self):
|
||||
if not self._commandQueue.empty() and not self.isStreaming():
|
||||
# We loop here to make sure that if we do NOT send the first command
|
||||
# from the queue, we'll send the second (if there is one). We do not
|
||||
# want to get stuck here by throwing away commands.
|
||||
while True:
|
||||
if self._commandQueue.empty() or self.isStreaming():
|
||||
# no command queue or irrelevant command queue => return
|
||||
return False
|
||||
|
||||
entry = self._commandQueue.get()
|
||||
if isinstance(entry, tuple):
|
||||
if not len(entry) == 2:
|
||||
return False
|
||||
# something with that entry is broken, ignore it and fetch
|
||||
# the next one
|
||||
continue
|
||||
cmd, cmd_type = entry
|
||||
else:
|
||||
cmd = entry
|
||||
cmd_type = None
|
||||
|
||||
self._sendCommand(cmd, cmd_type=cmd_type)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
if self._sendCommand(cmd, cmd_type=cmd_type):
|
||||
# we actually did add this cmd to the send queue, so let's
|
||||
# return, we are done here
|
||||
return True
|
||||
|
||||
def _detectPort(self, close):
|
||||
programmer = stk500v2.Stk500v2()
|
||||
|
|
@ -1314,6 +1344,11 @@ class MachineCom(object):
|
|||
return False
|
||||
|
||||
def _handleErrors(self, line):
|
||||
if line is None:
|
||||
return
|
||||
|
||||
lower_line = line.lower()
|
||||
|
||||
# No matter the state, if we see an error, goto the error state and store the error for reference.
|
||||
if line.startswith('Error:') or line.startswith('!!'):
|
||||
#Oh YEAH, consistency.
|
||||
|
|
@ -1323,14 +1358,18 @@ class MachineCom(object):
|
|||
if regex_minMaxError.match(line):
|
||||
line = line.rstrip() + self._readline()
|
||||
|
||||
if 'line number' in line.lower() or 'checksum' in line.lower() or 'format error' in line.lower() or 'expected line' in line.lower():
|
||||
if 'line number' in lower_line or 'checksum' in lower_line or 'format error' in lower_line or 'expected line' in lower_line:
|
||||
#Skip the communication errors, as those get corrected.
|
||||
self._lastCommError = line[6:] if line.startswith("Error:") else line[2:]
|
||||
pass
|
||||
elif 'volume.init' in line.lower() or "openroot" in line.lower() or 'workdir' in line.lower()\
|
||||
or "error writing to file" in line.lower():
|
||||
elif 'volume.init' in lower_line or "openroot" in lower_line or 'workdir' in lower_line\
|
||||
or "error writing to file" in lower_line or "cannot open" in lower_line\
|
||||
or "cannot enter" in lower_line:
|
||||
#Also skip errors with the SD card
|
||||
pass
|
||||
elif 'unknown command' in lower_line:
|
||||
#Ignore unkown command errors, it could be a typo or some missing feature
|
||||
pass
|
||||
elif not self.isError():
|
||||
self._errorValue = line[6:] if line.startswith("Error:") else line[2:]
|
||||
self._changeState(self.STATE_ERROR)
|
||||
|
|
@ -1471,7 +1510,7 @@ class MachineCom(object):
|
|||
# Make sure we are only handling one sending job at a time
|
||||
with self._sendingLock:
|
||||
if self._serial is None:
|
||||
return
|
||||
return False
|
||||
|
||||
gcode = None
|
||||
if not self.isStreaming():
|
||||
|
|
@ -1480,7 +1519,7 @@ class MachineCom(object):
|
|||
|
||||
if cmd is None:
|
||||
# command is no more, return
|
||||
return
|
||||
return False
|
||||
|
||||
if gcode and gcode in gcodeToEvent:
|
||||
# if this is a gcode bound to an event, trigger that now
|
||||
|
|
@ -1493,6 +1532,8 @@ class MachineCom(object):
|
|||
# trigger the "queued" phase only if we are not streaming to sd right now
|
||||
self._process_command_phase("queued", cmd, cmd_type, gcode=gcode)
|
||||
|
||||
return True
|
||||
|
||||
##~~ send loop handling
|
||||
|
||||
def _enqueue_for_sending(self, command, linenumber=None, command_type=None):
|
||||
|
|
@ -1545,7 +1586,14 @@ class MachineCom(object):
|
|||
command, _, gcode = self._process_command_phase("sending", command, command_type, gcode=gcode)
|
||||
|
||||
if command is None:
|
||||
# so no, we are not going to send this, that was a last-minute bail, let's fetch the next item from the queue
|
||||
# No, we are not going to send this, that was a last-minute bail.
|
||||
# However, since we already are in the send queue, our _monitor
|
||||
# loop won't be triggered with the reply from this unsent command
|
||||
# now, so we try to tickle the processing of any active
|
||||
# command queues manually
|
||||
self._continue_sending()
|
||||
|
||||
# and now let's fetch the next item from the queue
|
||||
continue
|
||||
|
||||
# now comes the part where we increase line numbers and send stuff - no turning back now
|
||||
|
|
@ -1564,9 +1612,14 @@ class MachineCom(object):
|
|||
if gcode is not None:
|
||||
use_up_clear = True
|
||||
|
||||
# if we need to use up a clear, do that now
|
||||
if use_up_clear:
|
||||
# if we need to use up a clear, do that now
|
||||
self._clear_to_send.clear()
|
||||
else:
|
||||
# Otherwise we need to tickle the read queue - there might not be a reply
|
||||
# to this command, so our _monitor loop will stay waiting until timeout. We
|
||||
# definitely do not want that, so we tickle the queue manually here
|
||||
self._continue_sending()
|
||||
|
||||
# now we just wait for the next clear and then start again
|
||||
self._clear_to_send.wait()
|
||||
|
|
@ -1707,6 +1760,12 @@ class MachineCom(object):
|
|||
if self.isPrinting() and not self.isSdPrinting():
|
||||
self.setPause(True)
|
||||
|
||||
def _gcode_M140_queuing(self, cmd, cmd_type=None):
|
||||
if not self._printerProfileManager.get_current_or_default()["heatedBed"]:
|
||||
self._log("Warn: Not sending \"{}\", printer profile has no heated bed".format(cmd))
|
||||
return None, # Don't send bed commands if we don't have a heated bed
|
||||
_gcode_M190_queuing = _gcode_M140_queuing
|
||||
|
||||
def _gcode_M104_sent(self, cmd, cmd_type=None):
|
||||
toolNum = self._currentTool
|
||||
toolMatch = regexes_parameters["intT"].search(cmd)
|
||||
|
|
@ -1780,7 +1839,8 @@ class MachineCom(object):
|
|||
# are going to shutdown the connection in a second anyhow.
|
||||
for tool in range(self._printerProfileManager.get_current_or_default()["extruder"]["count"]):
|
||||
self._doIncrementAndSendWithChecksum("M104 T{tool} S0".format(tool=tool))
|
||||
self._doIncrementAndSendWithChecksum("M140 S0")
|
||||
if self._printerProfileManager.get_current_or_default()["heatedBed"]:
|
||||
self._doIncrementAndSendWithChecksum("M140 S0")
|
||||
|
||||
# close to reset host state
|
||||
self._errorValue = "Closing serial port due to emergency stop M112."
|
||||
|
|
|
|||
108
src/octoprint/util/jinja.py
Normal file
108
src/octoprint/util/jinja.py
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
# coding=utf-8
|
||||
from __future__ import absolute_import
|
||||
|
||||
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
||||
__copyright__ = "Copyright (C) 2015 The OctoPrint Project - Released under terms of the AGPLv3 License"
|
||||
|
||||
import os
|
||||
|
||||
from jinja2.loaders import FileSystemLoader, PrefixLoader, ChoiceLoader, \
|
||||
TemplateNotFound, split_template_path
|
||||
|
||||
class FilteredFileSystemLoader(FileSystemLoader):
|
||||
"""
|
||||
Jinja2 ``FileSystemLoader`` subclass that allows filtering templates.
|
||||
|
||||
Only such templates will be accessible for whose paths the provided
|
||||
``path_filter`` filter function returns True.
|
||||
|
||||
``path_filter`` will receive the actual path on disc and should behave just
|
||||
like callables provided to Python's internal ``filter`` function, returning
|
||||
``True`` if the path is cleared and ``False`` if it is supposed to be removed
|
||||
from results and hence ``filter(path_filter, iterable)`` should be
|
||||
equivalent to ``[item for item in iterable if path_filter(item)]``.
|
||||
|
||||
If ``path_filter`` is not set or not a ``callable``, the loader will
|
||||
behave just like the regular Jinja2 ``FileSystemLoader``.
|
||||
"""
|
||||
def __init__(self, searchpath, path_filter=None, **kwargs):
|
||||
FileSystemLoader.__init__(self, searchpath, **kwargs)
|
||||
self.path_filter = path_filter
|
||||
|
||||
def get_source(self, environment, template):
|
||||
if callable(self.path_filter):
|
||||
pieces = split_template_path(template)
|
||||
if not self._combined_filter(os.path.join(*pieces)):
|
||||
raise TemplateNotFound(template)
|
||||
|
||||
return FileSystemLoader.get_source(self, environment, template)
|
||||
|
||||
def list_templates(self):
|
||||
result = FileSystemLoader.list_templates(self)
|
||||
|
||||
if callable(self.path_filter):
|
||||
result = sorted(filter(self._combined_filter, result))
|
||||
|
||||
return result
|
||||
|
||||
def _combined_filter(self, path):
|
||||
filter_results = map(lambda x: not os.path.exists(os.path.join(x, path)) or self.path_filter(os.path.join(x, path)),
|
||||
self.searchpath)
|
||||
return all(filter_results)
|
||||
|
||||
|
||||
def get_all_template_paths(loader):
|
||||
def walk_folder(folder):
|
||||
files = []
|
||||
walk_dir = os.walk(folder, followlinks=True)
|
||||
for dirpath, dirnames, filenames in walk_dir:
|
||||
for filename in filenames:
|
||||
path = os.path.join(dirpath, filename)
|
||||
files.append(path)
|
||||
return files
|
||||
|
||||
def collect_templates_for_loader(loader):
|
||||
if isinstance(loader, FilteredFileSystemLoader):
|
||||
result = []
|
||||
for folder in loader.searchpath:
|
||||
result += walk_folder(folder)
|
||||
return filter(loader.path_filter, result)
|
||||
|
||||
elif isinstance(loader, FileSystemLoader):
|
||||
result = []
|
||||
for folder in loader.searchpath:
|
||||
result += walk_folder(folder)
|
||||
return result
|
||||
|
||||
elif isinstance(loader, PrefixLoader):
|
||||
result = []
|
||||
for subloader in loader.mapping.values():
|
||||
result += collect_templates_for_loader(subloader)
|
||||
return result
|
||||
|
||||
elif isinstance(loader, ChoiceLoader):
|
||||
result = []
|
||||
for subloader in loader.loaders:
|
||||
result += collect_templates_for_loader(subloader)
|
||||
return result
|
||||
|
||||
return []
|
||||
|
||||
return collect_templates_for_loader(loader)
|
||||
|
||||
|
||||
def get_all_asset_paths(env):
|
||||
result = []
|
||||
for bundle in env:
|
||||
for content in bundle.resolve_contents():
|
||||
try:
|
||||
if not content:
|
||||
continue
|
||||
path = content[1]
|
||||
if not os.path.isfile(path):
|
||||
continue
|
||||
result.append(path)
|
||||
except IndexError:
|
||||
# intentionally ignored
|
||||
pass
|
||||
return result
|
||||
|
|
@ -309,8 +309,8 @@ class LocalStorageTest(unittest.TestCase):
|
|||
|
||||
@data(
|
||||
("some_file.gco", "some_file.gco"),
|
||||
("some_file with (parentheses) and ümläuts and digits 123.gco", "some_file_with_(parentheses)_and_mluts_and_digits_123.gco"),
|
||||
("pengüino pequeño.stl", "pengino_pequeo.stl")
|
||||
("some_file with (parentheses) and ümläuts and digits 123.gco", "some_file_with_(parentheses)_and_umlauts_and_digits_123.gco"),
|
||||
("pengüino pequeño.stl", "penguino_pequeno.stl")
|
||||
)
|
||||
@unpack
|
||||
def test_sanitize_name(self, input, expected):
|
||||
|
|
|
|||
1
tests/util/_files/jinja_test_data/.hidden_everywhere.txt
Normal file
1
tests/util/_files/jinja_test_data/.hidden_everywhere.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
hidden_everywhere
|
||||
1
tests/util/_files/jinja_test_data/normal_text.txt
Normal file
1
tests/util/_files/jinja_test_data/normal_text.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
normal_text
|
||||
1
tests/util/_files/jinja_test_data/not_a_text.dat
Normal file
1
tests/util/_files/jinja_test_data/not_a_text.dat
Normal file
|
|
@ -0,0 +1 @@
|
|||
not_a_text
|
||||
3
tests/util/_files/utf8_with_bom.txt
Normal file
3
tests/util/_files/utf8_with_bom.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# This is a text file encoded in UTF8 with a BOM
|
||||
|
||||
Here are some umlauts: äöüÄÖÜß
|
||||
3
tests/util/_files/utf8_without_bom.txt
Normal file
3
tests/util/_files/utf8_without_bom.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# This is a text file encoded in UTF8 without a BOM
|
||||
|
||||
Here are some umlauts: äöüÄÖÜß
|
||||
208
tests/util/test_file_helpers.py
Normal file
208
tests/util/test_file_helpers.py
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
# coding=utf-8
|
||||
from __future__ import absolute_import
|
||||
|
||||
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
||||
__copyright__ = "Copyright (C) 2015 The OctoPrint Project - Released under terms of the AGPLv3 License"
|
||||
|
||||
import unittest
|
||||
import mock
|
||||
import os
|
||||
import ddt
|
||||
import sys
|
||||
|
||||
import octoprint.util
|
||||
|
||||
class BomAwareOpenTest(unittest.TestCase):
|
||||
"""
|
||||
Tests for :func:`octoprint.util.bom_aware_open`.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.filename_utf8_with_bom = os.path.join(os.path.abspath(os.path.dirname(__file__)), "_files", "utf8_with_bom.txt")
|
||||
self.filename_utf8_without_bom = os.path.join(os.path.abspath(os.path.dirname(__file__)), "_files", "utf8_without_bom.txt")
|
||||
|
||||
def test_bom_aware_open_with_bom(self):
|
||||
"""Tests that the contents of a UTF8 file with BOM are loaded correctly (without the BOM)."""
|
||||
|
||||
# test
|
||||
with octoprint.util.bom_aware_open(self.filename_utf8_with_bom, encoding="utf-8") as f:
|
||||
contents = f.readlines()
|
||||
|
||||
# assert
|
||||
self.assertEquals(len(contents), 3)
|
||||
self.assertTrue(contents[0].startswith("#"))
|
||||
|
||||
def test_bom_aware_open_without_bom(self):
|
||||
"""Tests that the contents of a UTF8 file without BOM are loaded correctly."""
|
||||
|
||||
# test
|
||||
with octoprint.util.bom_aware_open(self.filename_utf8_without_bom, encoding="utf-8") as f:
|
||||
contents = f.readlines()
|
||||
|
||||
# assert
|
||||
self.assertEquals(len(contents), 3)
|
||||
self.assertTrue(contents[0].startswith("#"))
|
||||
|
||||
def test_bom_aware_open_ascii(self):
|
||||
"""Tests that the contents of a UTF8 file loaded as ASCII are replaced correctly if "replace" is specified on errors."""
|
||||
|
||||
# test
|
||||
with octoprint.util.bom_aware_open(self.filename_utf8_with_bom, errors="replace") as f:
|
||||
contents = f.readlines()
|
||||
|
||||
# assert
|
||||
self.assertEquals(len(contents), 3)
|
||||
self.assertTrue(contents[0].startswith(u"\ufffd" * 3 + "#"))
|
||||
self.assertTrue(contents[2].endswith(u"\ufffd\ufffd" * 6))
|
||||
|
||||
def test_bom_aware_open_encoding_error(self):
|
||||
"""Tests that an encoding error is thrown if not suppressed when opening a UTF8 file as ASCII."""
|
||||
try:
|
||||
with octoprint.util.bom_aware_open(self.filename_utf8_without_bom) as f:
|
||||
f.readlines()
|
||||
self.fail("Expected an exception")
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
|
||||
def test_bom_aware_open_parameters(self):
|
||||
"""Tests that the parameters are propagated properly."""
|
||||
|
||||
with mock.patch("codecs.open") as mock_open:
|
||||
with octoprint.util.bom_aware_open(self.filename_utf8_without_bom, mode="rb", encoding="utf-8", errors="ignore") as f:
|
||||
f.readlines()
|
||||
|
||||
mock_open.assert_called_once_with(self.filename_utf8_without_bom, encoding="utf-8", mode="rb", errors="ignore")
|
||||
|
||||
class TestAtomicWrite(unittest.TestCase):
|
||||
"""
|
||||
Tests for :func:`octoprint.util.atomic_write`.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
pass
|
||||
|
||||
@mock.patch("shutil.move")
|
||||
@mock.patch("tempfile.NamedTemporaryFile")
|
||||
def test_atomic_write(self, mock_tempfile, mock_move):
|
||||
"""Tests the regular basic "good" case."""
|
||||
|
||||
# setup
|
||||
mock_file = mock.MagicMock()
|
||||
mock_file.name = "tempfile.tmp"
|
||||
mock_tempfile.return_value = mock_file
|
||||
|
||||
# test
|
||||
with octoprint.util.atomic_write("somefile.yaml") as f:
|
||||
f.write("test")
|
||||
|
||||
# assert
|
||||
mock_tempfile.assert_called_once_with(mode="w+b", prefix="tmp", suffix="", delete=False)
|
||||
mock_file.write.assert_called_once_with("test")
|
||||
mock_file.close.assert_called_once_with()
|
||||
mock_move.assert_called_once_with("tempfile.tmp", "somefile.yaml")
|
||||
|
||||
@mock.patch("shutil.move")
|
||||
@mock.patch("tempfile.NamedTemporaryFile")
|
||||
def test_atomic_write_error_on_write(self, mock_tempfile, mock_move):
|
||||
"""Tests the error case where something in the wrapped code fails."""
|
||||
|
||||
# setup
|
||||
mock_file = mock.MagicMock()
|
||||
mock_file.name = "tempfile.tmp"
|
||||
mock_file.write.side_effect = RuntimeError()
|
||||
mock_tempfile.return_value = mock_file
|
||||
|
||||
# test
|
||||
try:
|
||||
with octoprint.util.atomic_write("somefile.yaml") as f:
|
||||
f.write("test")
|
||||
self.fail("Expected an exception")
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
# assert
|
||||
mock_tempfile.assert_called_once_with(mode="w+b", prefix="tmp", suffix="", delete=False)
|
||||
mock_file.close.assert_called_once_with()
|
||||
self.assertFalse(mock_move.called)
|
||||
|
||||
@mock.patch("shutil.move")
|
||||
@mock.patch("tempfile.NamedTemporaryFile")
|
||||
def test_atomic_write_error_on_move(self, mock_tempfile, mock_move):
|
||||
"""Tests the error case where the final move fails."""
|
||||
# setup
|
||||
mock_file = mock.MagicMock()
|
||||
mock_file.name = "tempfile.tmp"
|
||||
mock_tempfile.return_value = mock_file
|
||||
mock_move.side_effect = RuntimeError()
|
||||
|
||||
# test
|
||||
try:
|
||||
with octoprint.util.atomic_write("somefile.yaml") as f:
|
||||
f.write("test")
|
||||
self.fail("Expected an exception")
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
# assert
|
||||
mock_tempfile.assert_called_once_with(mode="w+b", prefix="tmp", suffix="", delete=False)
|
||||
mock_file.close.assert_called_once_with()
|
||||
self.assertTrue(mock_move.called)
|
||||
|
||||
@mock.patch("shutil.move")
|
||||
@mock.patch("tempfile.NamedTemporaryFile")
|
||||
def test_atomic_write_parameters(self, mock_tempfile, mock_move):
|
||||
"""Tests that the open parameters are propagated properly."""
|
||||
|
||||
# setup
|
||||
mock_file = mock.MagicMock()
|
||||
mock_file.name = "tempfile.tmp"
|
||||
mock_tempfile.return_value = mock_file
|
||||
|
||||
# test
|
||||
with octoprint.util.atomic_write("somefile.yaml", mode="w", prefix="foo", suffix="bar") as f:
|
||||
f.write("test")
|
||||
|
||||
# assert
|
||||
mock_tempfile.assert_called_once_with(mode="w", prefix="foo", suffix="bar", delete=False)
|
||||
mock_file.close.assert_called_once_with()
|
||||
mock_move.assert_called_once_with("tempfile.tmp", "somefile.yaml")
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class IsHiddenPathTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
import tempfile
|
||||
|
||||
self.basepath = tempfile.mkdtemp()
|
||||
|
||||
self.path_always_visible = os.path.join(self.basepath, "always_visible.txt")
|
||||
self.path_hidden_on_windows = os.path.join(self.basepath, "hidden_on_windows.txt")
|
||||
self.path_always_hidden = os.path.join(self.basepath, ".always_hidden.txt")
|
||||
|
||||
for attr in ("path_always_visible", "path_hidden_on_windows", "path_always_hidden"):
|
||||
path = getattr(self, attr)
|
||||
with open(path, "w+b") as f:
|
||||
f.write(attr)
|
||||
|
||||
import sys
|
||||
if sys.platform == "win32":
|
||||
# we use ctypes and the windows API to set the hidden attribute on the file
|
||||
# only hidden on windows
|
||||
import ctypes
|
||||
ctypes.windll.kernel32.SetFileAttributesW(unicode(self.path_hidden_on_windows), 2)
|
||||
|
||||
def tearDown(self):
|
||||
import shutil
|
||||
shutil.rmtree(self.basepath)
|
||||
|
||||
@ddt.data(
|
||||
(None, False),
|
||||
("path_always_visible", False),
|
||||
("path_always_hidden", True),
|
||||
("path_hidden_on_windows", sys.platform == "win32")
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_is_hidden_path(self, path_id, expected):
|
||||
path = getattr(self, path_id) if path_id is not None else None
|
||||
self.assertEqual(octoprint.util.is_hidden_path(path), expected)
|
||||
76
tests/util/test_jinja.py
Normal file
76
tests/util/test_jinja.py
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
# coding=utf-8
|
||||
from __future__ import absolute_import
|
||||
|
||||
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
||||
__copyright__ = "Copyright (C) 2015 The OctoPrint Project - Released under terms of the AGPLv3 License"
|
||||
|
||||
|
||||
import unittest
|
||||
import os
|
||||
import jinja2
|
||||
|
||||
from ddt import ddt, data, unpack
|
||||
|
||||
import octoprint.util.jinja
|
||||
|
||||
NONE_FILTER = None
|
||||
HIDDEN_FILTER = lambda x: not os.path.basename(x).startswith(".")
|
||||
NO_TXT_FILTER = lambda x: x.endswith(".txt")
|
||||
COMBINED_FILTER = lambda x: HIDDEN_FILTER(x) and NO_TXT_FILTER(x)
|
||||
|
||||
@ddt
|
||||
class FilteredFileSystemLoaderTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.basepath = os.path.join(os.path.abspath(os.path.dirname(__file__)), "_files", "jinja_test_data")
|
||||
self.environment = jinja2.Environment()
|
||||
|
||||
def loader_factory(self, path_filter):
|
||||
return octoprint.util.jinja.FilteredFileSystemLoader(self.basepath,
|
||||
path_filter=path_filter)
|
||||
|
||||
@data(
|
||||
(NONE_FILTER, [".hidden_everywhere.txt", "normal_text.txt", "not_a_text.dat"]),
|
||||
(HIDDEN_FILTER, ["normal_text.txt", "not_a_text.dat"]),
|
||||
(NO_TXT_FILTER, [".hidden_everywhere.txt", "normal_text.txt"]),
|
||||
(COMBINED_FILTER, ["normal_text.txt"])
|
||||
)
|
||||
@unpack
|
||||
def test_list_templates(self, path_filter, expected):
|
||||
loader = self.loader_factory(path_filter=path_filter)
|
||||
templates = loader.list_templates()
|
||||
self.assertListEqual(templates, expected)
|
||||
|
||||
@data(
|
||||
(NONE_FILTER, ((".hidden_everywhere.txt", True),
|
||||
("normal_text.txt", True),
|
||||
("not_a_text.dat", True))),
|
||||
(HIDDEN_FILTER, ((".hidden_everywhere.txt", False),
|
||||
("normal_text.txt", True),
|
||||
("not_a_text.dat", True))),
|
||||
(NO_TXT_FILTER, ((".hidden_everywhere.txt", True),
|
||||
("normal_text.txt", True),
|
||||
("not_a_text.dat", False))),
|
||||
(COMBINED_FILTER, ((".hidden_everywhere.txt", False),
|
||||
("normal_text.txt", True),
|
||||
("not_a_text.dat", False)))
|
||||
)
|
||||
@unpack
|
||||
def test_get_source_none_filter(self, path_filter, param_sets):
|
||||
loader = self.loader_factory(path_filter=path_filter)
|
||||
for param_set in param_sets:
|
||||
template, success = param_set
|
||||
if success:
|
||||
self._test_get_source_success(loader, template)
|
||||
else:
|
||||
self._test_get_source_notfound(loader, template)
|
||||
|
||||
def _test_get_source_success(self, loader, template):
|
||||
loader.get_source(self.environment, template)
|
||||
|
||||
def _test_get_source_notfound(self, loader, template):
|
||||
try:
|
||||
loader.get_source(self.environment, template)
|
||||
self.fail("Expected an exception")
|
||||
except jinja2.TemplateNotFound:
|
||||
pass
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue