Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ jobs:
python: 3.7
install:
- pip install setuptools==60.9.0 # https://github.com/pypa/setuptools/issues/3293
- pip install peerprint==0.3.0 # Needed for testing/mocking implementation
- pip install OctoPrint # Need OctoPrint to satisfy req's of `__init__.py`
- pip install coverage coveralls
- pip install -r requirements.txt
Expand Down
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
FROM python:3.7

# Installing ffmpeg is needed for working with timelapses - can be ommitted otherwise
# ZMQ libraries are peerprint dependencies - TODO need to find a way to auto install
# Also install vim for later edit based debugging
RUN apt-get update && apt-get -y install --no-install-recommends ffmpeg vim && rm -rf /var/lib/apt/lists/*
RUN apt-get update && apt-get -y install --no-install-recommends ffmpeg libczmq-dev libzmq5 vim && rm -rf /var/lib/apt/lists/*

# IPFS installation for LAN filesharing
RUN wget https://dist.ipfs.tech/kubo/v0.15.0/kubo_v0.15.0_linux-amd64.tar.gz \
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ This plugin automates your 3D printing!
* **Group multiple files together into "jobs" and run them multiple times.** Don't make 10 boxes by printing 10 bases, then 10 lids - just define a "box" job and print box/lid combos in sequence.
* **Reduce manual intervention with failure automation.** This plugin optionally integrates with [The Spaghetti Detective](https://www.thespaghettidetective.com/) and can retry prints that fail to adhere to the bed, with configurable limits on how hard to try before giving up.
* **Print with multiple 3D printers over the local network**. LAN queues can parallelize your printing efforts, while still providing a single queue to print from.
* **Automatically slice STL files before printing**. Integrates with [PrePrintService](https://plugins.octoprint.org/plugins/preprintservice/) and other OctoPrint slicer implementations so you can add STLs to the queue and slice them on-the-fly.

# Documentation

Expand Down
4 changes: 4 additions & 0 deletions continuousprint/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ class ContinuousprintPlugin(

# -------------------- Begin BlueprintPlugin --------------------

def is_blueprint_csrf_protected(self):
# See https://docs.octoprint.org/en/maintenance/plugins/mixins.html#blueprintplugin
return True

def get_blueprint(self):
# called before on_startup, but we need the plugin to provide the blueprint
self.on_startup()
Expand Down
33 changes: 27 additions & 6 deletions continuousprint/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
from enum import Enum
from octoprint.access.permissions import Permissions, ADMIN_GROUP
from octoprint.server.util.flask import restricted_access
from .queues.lan import ValidationError
from .queues.base import ValidationError
from .automation import getInterpreter, genEventScript
import flask
import json
from .storage import queries
from .storage.database import DEFAULT_QUEUE
from .data import CustomEvents
from .storage.database import DEFAULT_QUEUE, ARCHIVE_QUEUE
from .data import CustomEvents, Keys
from .driver import Action as DA
from abc import ABC, abstractmethod

Expand Down Expand Up @@ -141,7 +141,7 @@ def popup(self, msg, type="popup"):
return self._msg(dict(type=type, msg=msg))

def _sync(self, attr, data):
self._logger.debug(f"Refreshing UI {attr}")
# self._logger.debug(f"Refreshing UI {attr}")
msg = dict(type=f"set{attr}")
msg[attr] = data
self._msg(msg)
Expand Down Expand Up @@ -210,8 +210,11 @@ def add_job(self):
def mv_job(self):
src_id = flask.request.form["id"]
after_id = flask.request.form["after_id"]
before_id = flask.request.form.get("before_id")
if after_id == "": # Treat empty string as 'none' i.e. front of queue
after_id = None
if before_id == "": # Treat empty as 'none' i.e. end of queue
before_id = None
sq = self._get_queue(flask.request.form["src_queue"])
dq = self._get_queue(flask.request.form.get("dest_queue"))

Expand All @@ -225,7 +228,7 @@ def mv_job(self):
src_id = new_id

# Finally, move the job
dq.mv_job(src_id, after_id)
dq.mv_job(src_id, after_id, before_id)
return json.dumps("OK")

# PRIVATE API METHOD - may change without warning.
Expand Down Expand Up @@ -311,14 +314,32 @@ def reset_history(self):
@restricted_access
@cpq_permission(Permission.GETQUEUES)
def get_queues(self):
return json.dumps([q.as_dict() for q in queries.getQueues()])
qs = dict()
if self._get_key(Keys.NETWORK_QUEUES, False):
for n in self._peerprint.get_plugin().client.get_connections():
qs[n.network] = dict(
name=n.network, addr=n.addr, strategy="LINEAR", enabled=False
)

for q in queries.getQueues():
if q.name == DEFAULT_QUEUE:
qs[q.name] = q.as_dict()
qs[q.name]["enabled"] = True
qs[q.name]["rank"] = q.rank
elif q.name in qs:
qs[q.name]["enabled"] = True

qs = list(qs.values())
qs.sort(key=lambda q: q.get("rank", 99999999))
return json.dumps(qs)

# PRIVATE API METHOD - may change without warning.
@octoprint.plugin.BlueprintPlugin.route("/queues/edit", methods=["POST"])
@restricted_access
@cpq_permission(Permission.EDITQUEUES)
def edit_queues(self):
queues = json.loads(flask.request.form.get("json"))
queues = [q for q in queues if q["enabled"]] # strip disabled queues
(absent_names, added) = queries.assignQueues(queues)
self._commit_queues(added, absent_names)
return json.dumps("OK")
Expand Down
59 changes: 52 additions & 7 deletions continuousprint/api_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import json
import logging
from .driver import Action as DA
from .storage.database import DEFAULT_QUEUE
from unittest.mock import patch, MagicMock, call, PropertyMock
import imp
from flask import Flask
Expand Down Expand Up @@ -63,6 +64,7 @@ def kill_patches():
self.api._basefolder = "notexisty"
self.api._identifier = "continuousprint"
self.api._get_queue = MagicMock()
self.api._get_key = lambda k, d: d
self.api._logger = logging.getLogger()
self.app.register_blueprint(self.api.get_blueprint())
self.app.config.update({"TESTING": True})
Expand Down Expand Up @@ -165,14 +167,29 @@ def test_add_job(self):
self.assertEqual(rep.get_data(as_text=True), '"ret"')
self.api._get_queue().add_job.assert_called_with("jobname")

def test_mv_job(self):
def test_mv_job_no_before_id(self):
self.perm.PLUGIN_CONTINUOUSPRINT_EDITJOB.can.return_value = True
data = dict(id="foo", after_id="bar", src_queue="q1", dest_queue="q2")

rep = self.client.post("/job/mv", data=data)

self.assertEqual(rep.status_code, 200)
self.api._get_queue().mv_job.assert_called_with(data["id"], data["after_id"])
self.api._get_queue().mv_job.assert_called_with(
data["id"], data["after_id"], None
)

def test_mv_job(self):
self.perm.PLUGIN_CONTINUOUSPRINT_EDITJOB.can.return_value = True
data = dict(
id="foo", after_id="bar", before_id="baz", src_queue="q1", dest_queue="q2"
)

rep = self.client.post("/job/mv", data=data)

self.assertEqual(rep.status_code, 200)
self.api._get_queue().mv_job.assert_called_with(
data["id"], data["after_id"], data["before_id"]
)

def test_edit_job(self):
self.perm.PLUGIN_CONTINUOUSPRINT_EDITJOB.can.return_value = True
Expand Down Expand Up @@ -251,19 +268,47 @@ def test_reset_history(self, q):
@patch("continuousprint.api.queries")
def test_get_queues(self, q):
self.perm.PLUGIN_CONTINUOUSPRINT_GETQUEUES.can.return_value = True
mq = MagicMock()
mq.as_dict.return_value = dict(foo="bar")
q.getQueues.return_value = [mq]
self.api._get_key = lambda k, d: True

self.api._peerprint = MagicMock()
self.api._peerprint.get_plugin().client.get_connections.return_value = [
MagicMock(network="foo", addr="1234"),
MagicMock(network="bar", addr="1234"),
]

mq = MagicMock(rank=0)
mq.name = "foo"
mq.as_dict.return_value = dict(name="foo")
lq = MagicMock(rank=1)
lq.name = DEFAULT_QUEUE
lq.as_dict.return_value = dict(name=DEFAULT_QUEUE)
q.getQueues.return_value = [mq, lq]

rep = self.client.get("/queues/get")
self.assertEqual(rep.status_code, 200)
self.assertEqual(json.loads(rep.get_data(as_text=True)), [dict(foo="bar")])
self.assertEqual(
json.loads(rep.get_data(as_text=True)),
[
dict(name=DEFAULT_QUEUE, enabled=True, rank=1),
dict(name="foo", enabled=True, addr="1234", strategy="LINEAR"),
dict(name="bar", enabled=False, addr="1234", strategy="LINEAR"),
],
)

@patch("continuousprint.api.queries")
def test_edit_queues(self, q):
self.perm.PLUGIN_CONTINUOUSPRINT_EDITQUEUES.can.return_value = True
q.assignQueues.return_value = ("absent", "added")
self.api._commit_queues = MagicMock()
rep = self.client.post("/queues/edit", data=dict(json='"foo"'))
rep = self.client.post(
"/queues/edit",
data=dict(
json=json.dumps(
[dict(name="foo", enabled=True), dict(name="bar", enabled=False)]
)
),
)
q.assignQueues.assert_called_with([dict(name="foo", enabled=True)])
self.assertEqual(rep.status_code, 200)
self.assertEqual(rep.data, b'"OK"')
self.api._commit_queues.assert_called_with("added", "absent")
Expand Down
19 changes: 17 additions & 2 deletions continuousprint/data/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ class Keys(Enum):
BED_COOLDOWN_THRESHOLD = ("bed_cooldown_threshold", 30)
BED_COOLDOWN_TIMEOUT = ("bed_cooldown_timeout", 60)
MATERIAL_SELECTION = ("cp_material_selection_enabled", False)
NETWORK_NAME = ("cp_network_name", "Generic")
NETWORK_QUEUES = ("cp_network_queues_enabled", False)
AUTOMATION_TIMELAPSE_ACTION = (
"cp_automation_timelapse_action",
"do_nothing",
Expand Down Expand Up @@ -184,10 +184,25 @@ def __init__(self, setting, default):
"js/continuousprint_queue.js",
"js/continuousprint_viewmodel.js",
"js/continuousprint_settings_event.js",
"js/continuousprint_settings_automation.js",
"js/continuousprint_settings_queues.js",
"js/continuousprint_settings.js",
"js/continuousprint.js",
],
css=["css/continuousprint.css"],
css=[
"css/continuousprint_history.less.css",
"css/continuousprint_settings.less.css",
"css/continuousprint_shared.less.css",
"css/continuousprint_tab.less.css",
"css/continuousprint_themeify_compat.less.css",
],
less=[
"less/continuousprint_history.less",
"less/continuousprint_settings.less",
"less/continuousprint_shared.less",
"less/continuousprint_tab.less",
"less/continuousprint_themeify_compat.less",
],
)

TEMPLATES = [
Expand Down
Loading