Fix downloadPresenter and initial LicensePresenter.py code

CURA-6983
This commit is contained in:
Nino van Hooff 2020-01-09 16:56:53 +01:00
parent 028aece644
commit dda3d0b4eb
5 changed files with 196 additions and 43 deletions

View file

@ -13,6 +13,7 @@ import UM 1.1 as UM
UM.Dialog UM.Dialog
{ {
id: licenseDialog
title: catalog.i18nc("@title:window", "Plugin License Agreement") title: catalog.i18nc("@title:window", "Plugin License Agreement")
minimumWidth: UM.Theme.getSize("license_window_minimum").width minimumWidth: UM.Theme.getSize("license_window_minimum").width
minimumHeight: UM.Theme.getSize("license_window_minimum").height minimumHeight: UM.Theme.getSize("license_window_minimum").height
@ -21,16 +22,21 @@ UM.Dialog
property var pluginName; property var pluginName;
property var licenseContent; property var licenseContent;
property var pluginFileLocation; property var pluginFileLocation;
Item Item
{ {
anchors.fill: parent anchors.fill: parent
UM.I18nCatalog{id: catalog; name: "cura"}
Label Label
{ {
id: licenseTitle id: licenseTitle
anchors.top: parent.top anchors.top: parent.top
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
text: licenseDialog.pluginName + ": " + catalog.i18nc("@label", "This plugin contains a license.\nYou need to accept this license to install this plugin.\nDo you agree with the terms below?") text: licenseModel.title
wrapMode: Text.Wrap wrapMode: Text.Wrap
renderType: Text.NativeRendering renderType: Text.NativeRendering
} }
@ -43,7 +49,7 @@ UM.Dialog
anchors.right: parent.right anchors.right: parent.right
anchors.topMargin: UM.Theme.getSize("default_margin").height anchors.topMargin: UM.Theme.getSize("default_margin").height
readOnly: true readOnly: true
text: licenseDialog.licenseContent || "" text: licenseModel.licenseText
} }
} }
rightButtons: rightButtons:
@ -53,22 +59,14 @@ UM.Dialog
id: acceptButton id: acceptButton
anchors.margins: UM.Theme.getSize("default_margin").width anchors.margins: UM.Theme.getSize("default_margin").width
text: catalog.i18nc("@action:button", "Accept") text: catalog.i18nc("@action:button", "Accept")
onClicked: onClicked: handler.onLicenseAccepted
{
licenseDialog.close();
toolbox.install(licenseDialog.pluginFileLocation);
toolbox.subscribe(licenseDialog.pluginName);
}
}, },
Button Button
{ {
id: declineButton id: declineButton
anchors.margins: UM.Theme.getSize("default_margin").width anchors.margins: UM.Theme.getSize("default_margin").width
text: catalog.i18nc("@action:button", "Decline") text: catalog.i18nc("@action:button", "Decline")
onClicked: onClicked: handler.onLicenseDeclined
{
licenseDialog.close();
}
} }
] ]
} }

View file

@ -1,7 +1,7 @@
import os import os
import tempfile import tempfile
from functools import reduce from functools import reduce
from typing import Dict, List, Optional from typing import Dict, List, Optional, Any
from PyQt5.QtNetwork import QNetworkReply from PyQt5.QtNetwork import QNetworkReply
@ -30,7 +30,7 @@ class DownloadPresenter:
self._started = False self._started = False
self._progress_message = None # type: Optional[Message] self._progress_message = None # type: Optional[Message]
self._progress = {} # type: Dict[str, Dict[str, int]] # package_id, Dict self._progress = {} # type: Dict[str, Dict[str, Any]] # package_id, Dict
self._error = [] # type: List[str] # package_id self._error = [] # type: List[str] # package_id
def download(self, model: SubscribedPackagesModel): def download(self, model: SubscribedPackagesModel):
@ -41,26 +41,41 @@ class DownloadPresenter:
manager = HttpRequestManager.getInstance() manager = HttpRequestManager.getInstance()
for item in model.items: for item in model.items:
package_id = item["package_id"] package_id = item["package_id"]
request_data = manager.get(
item["download_url"],
callback = lambda reply, pid = package_id: self._onFinished(pid, reply),
download_progress_callback = lambda rx, rt, pid = package_id: self._onProgress(pid, rx, rt),
error_callback = lambda rx, rt, pid = package_id: self._onProgress(pid, rx, rt),
scope = self._scope)
self._progress[package_id] = { self._progress[package_id] = {
"received": 0, "received": 0,
"total": 1 # make sure this is not considered done yet. Also divByZero-safe "total": 1, # make sure this is not considered done yet. Also divByZero-safe
"file_written": None,
"request_data": request_data
} }
manager.get(
item["download_url"],
callback = lambda reply: self._onFinished(package_id, reply),
download_progress_callback = lambda rx, rt: self._onProgress(package_id, rx, rt),
error_callback = lambda rx, rt: self._onProgress(package_id, rx, rt),
scope = self._scope)
self._started = True self._started = True
self._showProgressMessage() self._showProgressMessage()
def abort(self):
manager = HttpRequestManager.getInstance()
for item in self._progress.values():
manager.abortRequest(item["request_data"])
# Aborts all current operations and returns a copy with the same settings such as app and scope
def resetCopy(self):
self.abort()
self.done.disconnectAll()
return DownloadPresenter(self._app)
def _showProgressMessage(self): def _showProgressMessage(self):
self._progress_message = Message(i18n_catalog.i18nc( self._progress_message = Message(i18n_catalog.i18nc(
"@info:generic", "@info:generic",
"\nSyncing..."), "\nSyncing..."),
lifetime = 0, lifetime = 0,
use_inactivity_timer=False,
progress = 0.0, progress = 0.0,
title = i18n_catalog.i18nc("@info:title", "Changes detected from your Ultimaker account", )) title = i18n_catalog.i18nc("@info:title", "Changes detected from your Ultimaker account", ))
self._progress_message.show() self._progress_message.show()
@ -68,13 +83,21 @@ class DownloadPresenter:
def _onFinished(self, package_id: str, reply: QNetworkReply): def _onFinished(self, package_id: str, reply: QNetworkReply):
self._progress[package_id]["received"] = self._progress[package_id]["total"] self._progress[package_id]["received"] = self._progress[package_id]["total"]
file_path = self._getTempFile(package_id) file_fd, file_path = tempfile.mkstemp()
os.close(file_fd) # close the file so we can open it from python
try: try:
with open(file_path) as temp_file: with open(file_path, "wb+") as temp_file:
# todo buffer this bytes_read = reply.read(256 * 1024)
temp_file.write(reply.readAll()) while bytes_read:
except IOError: temp_file.write(bytes_read)
bytes_read = reply.read(256 * 1024)
self._app.processEvents()
self._progress[package_id]["file_written"] = file_path
except IOError as e:
Logger.logException("e", "Failed to write downloaded package to temp file", e)
self._onError(package_id) self._onError(package_id)
temp_file.close()
self._checkDone() self._checkDone()
@ -90,8 +113,6 @@ class DownloadPresenter:
self._progress_message.setProgress(100.0 * (received / total)) # [0 .. 100] % self._progress_message.setProgress(100.0 * (received / total)) # [0 .. 100] %
self._checkDone()
def _onError(self, package_id: str): def _onError(self, package_id: str):
self._progress.pop(package_id) self._progress.pop(package_id)
self._error.append(package_id) self._error.append(package_id)
@ -99,16 +120,11 @@ class DownloadPresenter:
def _checkDone(self) -> bool: def _checkDone(self) -> bool:
for item in self._progress.values(): for item in self._progress.values():
if item["received"] != item["total"] or item["total"] == -1: if not item["file_written"]:
return False return False
success_items = {package_id : self._getTempFile(package_id) for package_id in self._progress.keys()} success_items = {package_id : value["file_written"] for package_id, value in self._progress.items()}
error_items = [package_id for package_id in self._error] error_items = [package_id for package_id in self._error]
self._progress_message.hide() self._progress_message.hide()
self.done.emit(success_items, error_items) self.done.emit(success_items, error_items)
def _getTempFile(self, package_id: str) -> str:
temp_dir = tempfile.gettempdir()
return os.path.join(temp_dir, package_id)

View file

@ -0,0 +1,30 @@
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal
# Model for the ToolboxLicenseDialog
class LicenseModel(QObject):
titleChanged = pyqtSignal()
licenseTextChanged = pyqtSignal()
def __init__(self, title: str = "", license_text: str = ""):
super().__init__()
self._title = title
self._license_text = license_text
@pyqtProperty(str, notify=titleChanged)
def title(self) -> str:
return self._title
def setTitle(self, title: str) -> None:
if self._title != title:
self._title = title
self.titleChanged.emit()
@pyqtProperty(str, notify=licenseTextChanged)
def licenseText(self) -> str:
return self._license_text
def setLicenseText(self, license_text: str) -> None:
if self._license_text != license_text:
self._license_text = license_text
self.licenseTextChanged.emit()

View file

@ -0,0 +1,89 @@
import os
from typing import Dict, Optional
from PyQt5.QtCore import QObject, pyqtSlot
from UM.PackageManager import PackageManager
from UM.Signal import Signal
from cura.CuraApplication import CuraApplication
from UM.i18n import i18nCatalog
from plugins.Toolbox.src.CloudSync.LicenseModel import LicenseModel
class LicensePresenter(QObject):
def __init__(self, app: CuraApplication):
super().__init__()
self._dialog = None #type: Optional[QObject]
self._package_manager = app.getPackageManager() # type: PackageManager
# Emits # todo
self.license_answers = Signal()
self._current_package_idx = 0
self._package_models = None # type: Optional[Dict]
self._app = app
self._compatibility_dialog_path = "resources/qml/dialogs/ToolboxLicenseDialog.qml"
## Show a license dialog for multiple packages where users can read a license and accept or decline them
# \param packages: Dict[package id, file path]
def present(self, plugin_path: str, packages: Dict[str, str]):
path = os.path.join(plugin_path, self._compatibility_dialog_path)
self._initState(packages)
if self._dialog is None:
context_properties = {
"catalog": i18nCatalog("cura"),
"licenseModel": LicenseModel("initial title", "initial text"),
"handler": self
}
self._dialog = self._app.createQmlComponent(path, context_properties)
self._present_current_package()
@pyqtSlot()
def onLicenseAccepted(self):
self._package_models[self._current_package_idx]["accepted"] = True
self._check_next_page()
@pyqtSlot()
def onLicenseDeclined(self):
self._package_models[self._current_package_idx]["accepted"] = False
self._check_next_page()
def _initState(self, packages: Dict[str, str]):
self._package_models = [
{
"package_id" : package_id,
"package_path" : package_path,
"accepted" : None #: None: no answer yet
}
for package_id, package_path in packages.items()
]
def _present_current_package(self):
package_model = self._package_models[self._current_package_idx]
license_content = self._package_manager.getPackageLicense(package_model["package_path"])
if license_content is None:
# implicitly accept when there is no license
self.onLicenseAccepted()
return
self._dialog.setProperty("licenseModel", LicenseModel("testTitle", "hoi"))
self._dialog.open() # does nothing if already open
def _check_next_page(self):
if self._current_package_idx + 1 < len(self._package_models):
self._current_package_idx += 1
self._present_current_package()
else:
self._dialog.close()
self.license_answers.emit(self._package_models)

View file

@ -1,9 +1,12 @@
from typing import List, Dict
from UM.Extension import Extension from UM.Extension import Extension
from UM.PluginRegistry import PluginRegistry from UM.PluginRegistry import PluginRegistry
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
from plugins.Toolbox import CloudPackageChecker from plugins.Toolbox import CloudPackageChecker
from plugins.Toolbox.src.CloudSync.DiscrepanciesPresenter import DiscrepanciesPresenter from plugins.Toolbox.src.CloudSync.DiscrepanciesPresenter import DiscrepanciesPresenter
from plugins.Toolbox.src.CloudSync.DownloadPresenter import DownloadPresenter from plugins.Toolbox.src.CloudSync.DownloadPresenter import DownloadPresenter
from plugins.Toolbox.src.CloudSync.LicensePresenter import LicensePresenter
from plugins.Toolbox.src.CloudSync.SubscribedPackagesModel import SubscribedPackagesModel from plugins.Toolbox.src.CloudSync.SubscribedPackagesModel import SubscribedPackagesModel
@ -15,8 +18,8 @@ from plugins.Toolbox.src.CloudSync.SubscribedPackagesModel import SubscribedPack
# the user selected to be performed # the user selected to be performed
# - The SyncOrchestrator uses PackageManager to remove local packages the users wants to see removed # - The SyncOrchestrator uses PackageManager to remove local packages the users wants to see removed
# - The DownloadPresenter shows a download progress dialog. It emits A tuple of succeeded and failed downloads # - The DownloadPresenter shows a download progress dialog. It emits A tuple of succeeded and failed downloads
# - The LicencePresenter extracts licences from the downloaded packages and presents a licence for each package to # - The LicensePresenter extracts licenses from the downloaded packages and presents a license for each package to
# - be installed. It emits the `licenceAnswers` {'packageId' : bool} for accept or declines # - be installed. It emits the `licenseAnswers` {'packageId' : bool} for accept or declines
# - The CloudPackageManager removes the declined packages from the account # - The CloudPackageManager removes the declined packages from the account
# - The SyncOrchestrator uses PackageManager to install the downloaded packages. # - The SyncOrchestrator uses PackageManager to install the downloaded packages.
# - Bliss / profit / done # - Bliss / profit / done
@ -24,18 +27,35 @@ class SyncOrchestrator(Extension):
def __init__(self, app: CuraApplication): def __init__(self, app: CuraApplication):
super().__init__() super().__init__()
self._name = "SyncOrchestrator" # Critical to differentiate This PluginObject from the Toolbox
self._checker = CloudPackageChecker(app) self._checker = CloudPackageChecker(app) # type: CloudPackageChecker
self._checker.discrepancies.connect(self._onDiscrepancies) self._checker.discrepancies.connect(self._onDiscrepancies)
self._discrepanciesPresenter = DiscrepanciesPresenter(app) self._discrepanciesPresenter = DiscrepanciesPresenter(app) # type: DiscrepanciesPresenter
self._discrepanciesPresenter.packageMutations.connect(self._onPackageMutations) self._discrepanciesPresenter.packageMutations.connect(self._onPackageMutations)
self._downloadPresenter = DownloadPresenter(app) self._downloadPresenter = DownloadPresenter(app) # type: DownloadPresenter
self._licensePresenter = LicensePresenter(app) # type: LicensePresenter
def _onDiscrepancies(self, model: SubscribedPackagesModel): def _onDiscrepancies(self, model: SubscribedPackagesModel):
plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId()) # todo revert
self._discrepanciesPresenter.present(plugin_path, model) self._onDownloadFinished({"SupportEraser" : "/home/nvanhooff/Downloads/ThingiBrowser-v7.0.0-2019-12-12T18_24_40Z.curapackage"}, [])
# plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId())
# self._discrepanciesPresenter.present(plugin_path, model)
def _onPackageMutations(self, mutations: SubscribedPackagesModel): def _onPackageMutations(self, mutations: SubscribedPackagesModel):
self._downloadPresenter = self._downloadPresenter.resetCopy()
self._downloadPresenter.done.connect(self._onDownloadFinished)
self._downloadPresenter.download(mutations) self._downloadPresenter.download(mutations)
## When a set of packages have finished downloading
# \param success_items: Dict[package_id, file_path]
# \param error_items: List[package_id]
def _onDownloadFinished(self, success_items: Dict[str, str], error_items: List[str]):
# todo handle error items
plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId())
self._licensePresenter.present(plugin_path, success_items)