Merge pull request #5912 from Ultimaker/cloud-print-job-actions

CS-134: Print job actions via cloud
This commit is contained in:
Ian Paschal 2019-06-25 15:52:10 +02:00 committed by GitHub
commit 48a69afbcf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 80 additions and 51 deletions

View File

@ -81,7 +81,7 @@ Item
enabled: visible && !(printJob.state == "pausing" || printJob.state == "resuming"); enabled: visible && !(printJob.state == "pausing" || printJob.state == "resuming");
onClicked: { onClicked: {
if (printJob.state == "paused") { if (printJob.state == "paused") {
printJob.setState("print"); printJob.setState("resume");
popUp.close(); popUp.close();
return; return;
} }

View File

@ -22,10 +22,6 @@ Item
// The print job which all other data is derived from // The print job which all other data is derived from
property var printJob: null property var printJob: null
// If the printer is a cloud printer or not. Other items base their enabled state off of this boolean. In the future
// they might not need to though.
property bool cloudConnection: Cura.MachineManager.activeMachineIsUsingCloudConnection
width: parent.width width: parent.width
height: childrenRect.height height: childrenRect.height
@ -217,7 +213,7 @@ Item
} }
width: 32 * screenScaleFactor // TODO: Theme! width: 32 * screenScaleFactor // TODO: Theme!
height: 32 * screenScaleFactor // TODO: Theme! height: 32 * screenScaleFactor // TODO: Theme!
enabled: !cloudConnection enabled: OutputDevice.supportsPrintJobActions
onClicked: enabled ? contextMenu.switchPopupState() : {} onClicked: enabled ? contextMenu.switchPopupState() : {}
visible: visible:
{ {
@ -250,7 +246,7 @@ Item
MonitorInfoBlurb MonitorInfoBlurb
{ {
id: contextMenuDisabledInfo id: contextMenuDisabledInfo
text: catalog.i18nc("@info", "These options are not available because you are monitoring a cloud printer.") text: catalog.i18nc("@info", "Please update your printer's firmware to manage the queue remotely.")
target: contextMenuButton target: contextMenuButton
} }
} }

View File

@ -172,8 +172,7 @@ Item
} }
width: 36 * screenScaleFactor // TODO: Theme! width: 36 * screenScaleFactor // TODO: Theme!
height: 36 * screenScaleFactor // TODO: Theme! height: 36 * screenScaleFactor // TODO: Theme!
enabled: !cloudConnection enabled: OutputDevice.supportsPrintJobActions
onClicked: enabled ? contextMenu.switchPopupState() : {} onClicked: enabled ? contextMenu.switchPopupState() : {}
visible: visible:
{ {
@ -206,7 +205,7 @@ Item
MonitorInfoBlurb MonitorInfoBlurb
{ {
id: contextMenuDisabledInfo id: contextMenuDisabledInfo
text: catalog.i18nc("@info", "These options are not available because you are monitoring a cloud printer.") text: catalog.i18nc("@info", "Please update your printer's firmware to manage the queue remotely.")
target: contextMenuButton target: contextMenuButton
} }
@ -244,7 +243,6 @@ Item
} }
} }
// Divider // Divider
Rectangle Rectangle
{ {

View File

@ -42,7 +42,6 @@ Item
} }
height: 18 * screenScaleFactor // TODO: Theme! height: 18 * screenScaleFactor // TODO: Theme!
width: childrenRect.width width: childrenRect.width
visible: !cloudConnection
UM.RecolorImage UM.RecolorImage
{ {
@ -65,7 +64,7 @@ Item
color: UM.Theme.getColor("monitor_text_link") color: UM.Theme.getColor("monitor_text_link")
font: UM.Theme.getFont("medium") // 14pt, regular font: UM.Theme.getFont("medium") // 14pt, regular
linkColor: UM.Theme.getColor("monitor_text_link") linkColor: UM.Theme.getColor("monitor_text_link")
text: catalog.i18nc("@label link to connect manager", "Go to Cura Connect") text: catalog.i18nc("@label link to connect manager", "Manage in browser")
renderType: Text.NativeRendering renderType: Text.NativeRendering
} }
} }
@ -73,9 +72,7 @@ Item
MouseArea MouseArea
{ {
anchors.fill: manageQueueLabel anchors.fill: manageQueueLabel
enabled: !cloudConnection onClicked: OutputDevice.openPrintJobControlPanel()
hoverEnabled: !cloudConnection
onClicked: Cura.MachineManager.printerOutputDevices[0].openPrintJobControlPanel()
onEntered: onEntered:
{ {
manageQueueText.font.underline = true manageQueueText.font.underline = true
@ -198,8 +195,7 @@ Item
color: UM.Theme.getColor("monitor_card_background") color: UM.Theme.getColor("monitor_card_background")
border.color: UM.Theme.getColor("monitor_card_border") border.color: UM.Theme.getColor("monitor_card_border")
radius: 2 * screenScaleFactor // TODO: Theme! radius: 2 * screenScaleFactor // TODO: Theme!
visible: OutputDevice.printJobs.length == 0
visible: printJobList.model.length == 0
Row Row
{ {
@ -249,14 +245,14 @@ Item
color: UM.Theme.getColor("monitor_text_link") color: UM.Theme.getColor("monitor_text_link")
font: UM.Theme.getFont("medium") // 14pt, regular font: UM.Theme.getFont("medium") // 14pt, regular
linkColor: UM.Theme.getColor("monitor_text_link") linkColor: UM.Theme.getColor("monitor_text_link")
text: catalog.i18nc("@label link to connect manager", "View print history") text: catalog.i18nc("@label link to connect manager", "Manage in browser")
renderType: Text.NativeRendering renderType: Text.NativeRendering
} }
MouseArea MouseArea
{ {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
onClicked: Cura.MachineManager.printerOutputDevices[0].openPrintJobControlPanel() onClicked: OutputDevice.openPrintJobControlPanel()
onEntered: onEntered:
{ {
viewPrintHistoryText.font.underline = true viewPrintHistoryText.font.underline = true

View File

@ -96,6 +96,21 @@ class CloudApiClient:
reply = self._manager.post(self._createEmptyRequest(url), b"") reply = self._manager.post(self._createEmptyRequest(url), b"")
self._addCallback(reply, on_finished, CloudPrintResponse) self._addCallback(reply, on_finished, CloudPrintResponse)
## Send a print job action to the cluster for the given print job.
# \param cluster_id: The ID of the cluster.
# \param cluster_job_id: The ID of the print job within the cluster.
# \param action: The name of the action to execute.
def doPrintJobAction(self, cluster_id: str, cluster_job_id: str, action: str, data: Optional[Dict[str, Any]] = None) -> None:
body = b""
if data:
try:
body = json.dumps({"data": data}).encode()
except JSONDecodeError as err:
Logger.log("w", "Could not encode body: %s", err)
return
url = "{}/clusters/{}/print_jobs/{}/action/{}".format(self.CLUSTER_API_ROOT, cluster_id, cluster_job_id, action)
self._manager.post(self._createEmptyRequest(url), body)
## We override _createEmptyRequest in order to add the user credentials. ## We override _createEmptyRequest in order to add the user credentials.
# \param url: The URL to request # \param url: The URL to request
# \param content_type: The type of the body contents. # \param content_type: The type of the body contents.

View File

@ -1,5 +1,6 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from cura.PrinterOutput.Models.PrintJobOutputModel import PrintJobOutputModel
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@ -13,10 +14,13 @@ class CloudOutputController(PrinterOutputController):
# The cloud connection only supports fetching the printer and queue status and adding a job to the queue. # The cloud connection only supports fetching the printer and queue status and adding a job to the queue.
# To let the UI know this we mark all features below as False. # To let the UI know this we mark all features below as False.
self.can_pause = False self.can_pause = True
self.can_abort = False self.can_abort = True
self.can_pre_heat_bed = False self.can_pre_heat_bed = False
self.can_pre_heat_hotends = False self.can_pre_heat_hotends = False
self.can_send_raw_gcode = False self.can_send_raw_gcode = False
self.can_control_manually = False self.can_control_manually = False
self.can_update_firmware = False self.can_update_firmware = False
def setJobState(self, job: "PrintJobOutputModel", state: str):
self._output_device.setJobState(job.key, state)

View File

@ -6,6 +6,7 @@ from time import time
from typing import Dict, List, Optional, Set, cast from typing import Dict, List, Optional, Set, cast
from PyQt5.QtCore import QObject, QUrl, pyqtProperty, pyqtSignal, pyqtSlot from PyQt5.QtCore import QObject, QUrl, pyqtProperty, pyqtSignal, pyqtSlot
from PyQt5.QtGui import QDesktopServices
from UM import i18nCatalog from UM import i18nCatalog
from UM.Backend.Backend import BackendState from UM.Backend.Backend import BackendState
@ -15,6 +16,7 @@ from UM.Message import Message
from UM.PluginRegistry import PluginRegistry from UM.PluginRegistry import PluginRegistry
from UM.Qt.Duration import Duration, DurationFormat from UM.Qt.Duration import Duration, DurationFormat
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
from UM.Version import Version
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
from cura.PrinterOutput.NetworkedPrinterOutputDevice import AuthState, NetworkedPrinterOutputDevice from cura.PrinterOutput.NetworkedPrinterOutputDevice import AuthState, NetworkedPrinterOutputDevice
@ -48,6 +50,9 @@ class CloudOutputDevice(NetworkedPrinterOutputDevice):
# The interval with which the remote clusters are checked # The interval with which the remote clusters are checked
CHECK_CLUSTER_INTERVAL = 10.0 # seconds CHECK_CLUSTER_INTERVAL = 10.0 # seconds
# The minimum version of firmware that support print job actions over cloud.
PRINT_JOB_ACTIONS_MIN_VERSION = Version("5.3.0")
# Signal triggered when the print jobs in the queue were changed. # Signal triggered when the print jobs in the queue were changed.
printJobsChanged = pyqtSignal() printJobsChanged = pyqtSignal()
@ -359,6 +364,13 @@ class CloudOutputDevice(NetworkedPrinterOutputDevice):
).show() ).show()
self.writeFinished.emit() self.writeFinished.emit()
## Whether the printer that this output device represents supports print job actions via the cloud.
@pyqtProperty(bool, notify = _clusterPrintersChanged)
def supportsPrintJobActions(self) -> bool:
version_number = self.printers[0].firmwareVersion.split(".")
firmware_version = Version([version_number[0], version_number[1], version_number[2]])
return firmware_version >= self.PRINT_JOB_ACTIONS_MIN_VERSION
## Gets the number of printers in the cluster. ## Gets the number of printers in the cluster.
# We use a minimum of 1 because cloud devices are always a cluster and printer discovery needs it. # We use a minimum of 1 because cloud devices are always a cluster and printer discovery needs it.
@pyqtProperty(int, notify = _clusterPrintersChanged) @pyqtProperty(int, notify = _clusterPrintersChanged)
@ -399,6 +411,22 @@ class CloudOutputDevice(NetworkedPrinterOutputDevice):
return [print_job for print_job in self._print_jobs if return [print_job for print_job in self._print_jobs if
print_job.assignedPrinter is not None and print_job.state != "queued"] print_job.assignedPrinter is not None and print_job.state != "queued"]
def setJobState(self, print_job_uuid: str, state: str) -> None:
self._api.doPrintJobAction(self._cluster.cluster_id, print_job_uuid, state)
@pyqtSlot(str)
def sendJobToTop(self, print_job_uuid: str) -> None:
self._api.doPrintJobAction(self._cluster.cluster_id, print_job_uuid, "move",
{"list": "queued", "to_position": 0})
@pyqtSlot(str)
def deleteJobFromQueue(self, print_job_uuid: str) -> None:
self._api.doPrintJobAction(self._cluster.cluster_id, print_job_uuid, "remove")
@pyqtSlot(str)
def forceSendJob(self, print_job_uuid: str) -> None:
self._api.doPrintJobAction(self._cluster.cluster_id, print_job_uuid, "force")
@pyqtSlot(int, result = str) @pyqtSlot(int, result = str)
def formatDuration(self, seconds: int) -> str: def formatDuration(self, seconds: int) -> str:
return Duration(seconds).getDisplayString(DurationFormat.Format.Short) return Duration(seconds).getDisplayString(DurationFormat.Format.Short)
@ -411,6 +439,18 @@ class CloudOutputDevice(NetworkedPrinterOutputDevice):
def getDateCompleted(self, time_remaining: int) -> str: def getDateCompleted(self, time_remaining: int) -> str:
return formatDateCompleted(time_remaining) return formatDateCompleted(time_remaining)
@pyqtProperty(bool, notify=printJobsChanged)
def receivedPrintJobs(self) -> bool:
return bool(self._print_jobs)
@pyqtSlot()
def openPrintJobControlPanel(self) -> None:
QDesktopServices.openUrl(QUrl("https://mycloud.ultimaker.com"))
@pyqtSlot()
def openPrinterControlPanel(self) -> None:
QDesktopServices.openUrl(QUrl("https://mycloud.ultimaker.com"))
## TODO: The following methods are required by the monitor page QML, but are not actually available using cloud. ## TODO: The following methods are required by the monitor page QML, but are not actually available using cloud.
# TODO: We fake the methods here to not break the monitor page. # TODO: We fake the methods here to not break the monitor page.
@ -422,30 +462,6 @@ class CloudOutputDevice(NetworkedPrinterOutputDevice):
def setActiveCameraUrl(self, camera_url: "QUrl") -> None: def setActiveCameraUrl(self, camera_url: "QUrl") -> None:
pass pass
@pyqtProperty(bool, notify = printJobsChanged)
def receivedPrintJobs(self) -> bool:
return bool(self._print_jobs)
@pyqtSlot()
def openPrintJobControlPanel(self) -> None:
pass
@pyqtSlot()
def openPrinterControlPanel(self) -> None:
pass
@pyqtSlot(str)
def sendJobToTop(self, print_job_uuid: str) -> None:
pass
@pyqtSlot(str)
def deleteJobFromQueue(self, print_job_uuid: str) -> None:
pass
@pyqtSlot(str)
def forceSendJob(self, print_job_uuid: str) -> None:
pass
@pyqtProperty("QVariantList", notify = _clusterPrintersChanged) @pyqtProperty("QVariantList", notify = _clusterPrintersChanged)
def connectedPrintersTypeCount(self) -> List[Dict[str, str]]: def connectedPrintersTypeCount(self) -> List[Dict[str, str]]:
return [] return []

View File

@ -91,7 +91,6 @@ class CloudClusterPrintJobStatus(BaseCloudModel):
def createOutputModel(self, controller: CloudOutputController) -> UM3PrintJobOutputModel: def createOutputModel(self, controller: CloudOutputController) -> UM3PrintJobOutputModel:
model = UM3PrintJobOutputModel(controller, self.uuid, self.name) model = UM3PrintJobOutputModel(controller, self.uuid, self.name)
self.updateOutputModel(model) self.updateOutputModel(model)
return model return model
## Creates a new configuration model ## Creates a new configuration model

View File

@ -140,6 +140,11 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
if self._printer_selection_dialog is not None: if self._printer_selection_dialog is not None:
self._printer_selection_dialog.show() self._printer_selection_dialog.show()
## Whether the printer that this output device represents supports print job actions via the local network.
@pyqtProperty(bool, constant=True)
def supportsPrintJobActions(self) -> bool:
return True
@pyqtProperty(int, constant=True) @pyqtProperty(int, constant=True)
def clusterSize(self) -> int: def clusterSize(self) -> int:
return self._cluster_size return self._cluster_size

View File

@ -27,7 +27,7 @@ from UM.Version import Version
from . import ClusterUM3OutputDevice, LegacyUM3OutputDevice from . import ClusterUM3OutputDevice, LegacyUM3OutputDevice
from .Cloud.CloudOutputDeviceManager import CloudOutputDeviceManager from .Cloud.CloudOutputDeviceManager import CloudOutputDeviceManager
from .Cloud.CloudOutputDevice import CloudOutputDevice # typing from .Cloud.CloudOutputDevice import CloudOutputDevice # typing
if TYPE_CHECKING: if TYPE_CHECKING:
from PyQt5.QtNetwork import QNetworkReply from PyQt5.QtNetwork import QNetworkReply

View File

@ -72,9 +72,9 @@ class TestCloudOutputDevice(TestCase):
controller_fields = { controller_fields = {
"_output_device": self.device, "_output_device": self.device,
"can_abort": False, "can_abort": True,
"can_control_manually": False, "can_control_manually": False,
"can_pause": False, "can_pause": True,
"can_pre_heat_bed": False, "can_pre_heat_bed": False,
"can_pre_heat_hotends": False, "can_pre_heat_hotends": False,
"can_send_raw_gcode": False, "can_send_raw_gcode": False,