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
{
id: licenseDialog
title: catalog.i18nc("@title:window", "Plugin License Agreement")
minimumWidth: UM.Theme.getSize("license_window_minimum").width
minimumHeight: UM.Theme.getSize("license_window_minimum").height
@ -21,16 +22,21 @@ UM.Dialog
property var pluginName;
property var licenseContent;
property var pluginFileLocation;
Item
{
anchors.fill: parent
UM.I18nCatalog{id: catalog; name: "cura"}
Label
{
id: licenseTitle
anchors.top: parent.top
anchors.left: parent.left
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
renderType: Text.NativeRendering
}
@ -43,7 +49,7 @@ UM.Dialog
anchors.right: parent.right
anchors.topMargin: UM.Theme.getSize("default_margin").height
readOnly: true
text: licenseDialog.licenseContent || ""
text: licenseModel.licenseText
}
}
rightButtons:
@ -53,22 +59,14 @@ UM.Dialog
id: acceptButton
anchors.margins: UM.Theme.getSize("default_margin").width
text: catalog.i18nc("@action:button", "Accept")
onClicked:
{
licenseDialog.close();
toolbox.install(licenseDialog.pluginFileLocation);
toolbox.subscribe(licenseDialog.pluginName);
}
onClicked: handler.onLicenseAccepted
},
Button
{
id: declineButton
anchors.margins: UM.Theme.getSize("default_margin").width
text: catalog.i18nc("@action:button", "Decline")
onClicked:
{
licenseDialog.close();
}
onClicked: handler.onLicenseDeclined
}
]
}

View file

@ -1,7 +1,7 @@
import os
import tempfile
from functools import reduce
from typing import Dict, List, Optional
from typing import Dict, List, Optional, Any
from PyQt5.QtNetwork import QNetworkReply
@ -30,7 +30,7 @@ class DownloadPresenter:
self._started = False
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
def download(self, model: SubscribedPackagesModel):
@ -41,26 +41,41 @@ class DownloadPresenter:
manager = HttpRequestManager.getInstance()
for item in model.items:
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] = {
"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._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):
self._progress_message = Message(i18n_catalog.i18nc(
"@info:generic",
"\nSyncing..."),
lifetime = 0,
use_inactivity_timer=False,
progress = 0.0,
title = i18n_catalog.i18nc("@info:title", "Changes detected from your Ultimaker account", ))
self._progress_message.show()
@ -68,13 +83,21 @@ class DownloadPresenter:
def _onFinished(self, package_id: str, reply: QNetworkReply):
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:
with open(file_path) as temp_file:
# todo buffer this
temp_file.write(reply.readAll())
except IOError:
with open(file_path, "wb+") as temp_file:
bytes_read = reply.read(256 * 1024)
while bytes_read:
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)
temp_file.close()
self._checkDone()
@ -90,8 +113,6 @@ class DownloadPresenter:
self._progress_message.setProgress(100.0 * (received / total)) # [0 .. 100] %
self._checkDone()
def _onError(self, package_id: str):
self._progress.pop(package_id)
self._error.append(package_id)
@ -99,16 +120,11 @@ class DownloadPresenter:
def _checkDone(self) -> bool:
for item in self._progress.values():
if item["received"] != item["total"] or item["total"] == -1:
if not item["file_written"]:
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]
self._progress_message.hide()
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.PluginRegistry import PluginRegistry
from cura.CuraApplication import CuraApplication
from plugins.Toolbox import CloudPackageChecker
from plugins.Toolbox.src.CloudSync.DiscrepanciesPresenter import DiscrepanciesPresenter
from plugins.Toolbox.src.CloudSync.DownloadPresenter import DownloadPresenter
from plugins.Toolbox.src.CloudSync.LicensePresenter import LicensePresenter
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 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 LicencePresenter extracts licences from the downloaded packages and presents a licence for each package to
# - be installed. It emits the `licenceAnswers` {'packageId' : bool} for accept or declines
# - The LicensePresenter extracts licenses from the downloaded packages and presents a license for each package to
# - be installed. It emits the `licenseAnswers` {'packageId' : bool} for accept or declines
# - The CloudPackageManager removes the declined packages from the account
# - The SyncOrchestrator uses PackageManager to install the downloaded packages.
# - Bliss / profit / done
@ -24,18 +27,35 @@ class SyncOrchestrator(Extension):
def __init__(self, app: CuraApplication):
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._discrepanciesPresenter = DiscrepanciesPresenter(app)
self._discrepanciesPresenter = DiscrepanciesPresenter(app) # type: DiscrepanciesPresenter
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):
plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId())
self._discrepanciesPresenter.present(plugin_path, model)
# todo revert
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):
self._downloadPresenter = self._downloadPresenter.resetCopy()
self._downloadPresenter.done.connect(self._onDownloadFinished)
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)