From 4bb1e0c7729d5ec7b6d4cb1f21ff4dfd4f71b5b0 Mon Sep 17 00:00:00 2001 From: Roula O'Regan Date: Fri, 19 Nov 2021 19:07:15 -0800 Subject: [PATCH 1/4] Limit non job owners interaction Check user permissions before allowing a user to "eat, kill, retry" jobs that they do not own. Allow an override in "File -> Enable Job Interaction". This gets saved to the config file and is disabled by default. --- cuegui/cuegui/Constants.py | 2 + cuegui/cuegui/MainWindow.py | 34 ++++- cuegui/cuegui/MenuActions.py | 261 +++++++++++++++++++++++------------ cuegui/cuegui/Utils.py | 23 +++ 4 files changed, 230 insertions(+), 90 deletions(-) diff --git a/cuegui/cuegui/Constants.py b/cuegui/cuegui/Constants.py index 13b511317..b25ae0e50 100644 --- a/cuegui/cuegui/Constants.py +++ b/cuegui/cuegui/Constants.py @@ -137,6 +137,8 @@ def __packaged_version(): EMAIL_BODY_PREFIX = __config.get('email.body_prefix') EMAIL_BODY_SUFFIX = __config.get('email.body_suffix') EMAIL_DOMAIN = __config.get('email.domain') +USER_CONFIRM_RESTART = "You must restart for this action to take effect, close window?: " +USER_INTERACTION_PERMISSIONS = "You do not have permissions to {0} {1} owned by {2}" GITHUB_CREATE_ISSUE_URL = __config.get('links.issue.create') URL_USERGUIDE = __config.get('links.user_guide') diff --git a/cuegui/cuegui/MainWindow.py b/cuegui/cuegui/MainWindow.py index 787458ea2..1cbb7cd6b 100644 --- a/cuegui/cuegui/MainWindow.py +++ b/cuegui/cuegui/MainWindow.py @@ -68,6 +68,7 @@ def __init__(self, app_name, app_version, window_name, parent = None): self.name = window_name else: self.name = self.windows_names[0] + self.__isEnabled = bool(int(QtGui.qApp.settings.value("EnableJobInteraction", 0))) # Provides a location for widgets to the right of the menu menuLayout = QtWidgets.QHBoxLayout() @@ -195,6 +196,20 @@ def __createMenus(self): self.windowMenu = self.menuBar().addMenu("&Window") self.helpMenu = self.menuBar().addMenu("&Help") + if self.__isEnabled is False: + # Menu Bar: File -> Enable Job Interaction + enableJobInteraction = QtWidgets.QAction(QtGui.QIcon('icons/exit.png'), '&Enable Job Interaction', self) + enableJobInteraction.setStatusTip('Enable Job Interaction') + enableJobInteraction.triggered.connect(self.__enableJobInteraction) + self.fileMenu.addAction(enableJobInteraction) + # allow user to disable the job interaction + else: + # Menu Bar: File -> Disable Job Interaction + enableJobInteraction = QtWidgets.QAction(QtGui.QIcon('icons/exit.png'), '&Disable Job Interaction', self) + enableJobInteraction.setStatusTip('Disable Job Interaction') + enableJobInteraction.triggered.connect(self.__enableJobInteraction) + self.fileMenu.addAction(enableJobInteraction) + # Menu Bar: File -> Close Window close = QtWidgets.QAction(QtGui.QIcon('icons/exit.png'), '&Close Window', self) close.setStatusTip('Close Window') @@ -454,9 +469,26 @@ def __revertLayout(self): result = QtWidgets.QMessageBox.question( self, "Restart required ", - "You must restart for this action to take effect, close window?: ", + cuegui.Constants.USER_CONFIRM_RESTART, QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) if result == QtWidgets.QMessageBox.Yes: self.settings.setValue("RevertLayout", True) self.__windowCloseApplication() + + def __enableJobInteraction(self): + """ Enable/Disable user job interaction """ + result = QtWidgets.QMessageBox.question( + self, + "Job Interaction Settings ", + cuegui.Constants.USER_CONFIRM_RESTART, + QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) + + if result == QtWidgets.QMessageBox.Yes: + # currently not enabled, user wants to enable + if self.__isEnabled is False: + self.settings.setValue("EnableJobInteraction", 1) + self.__windowCloseApplication() + else: + self.settings.setValue("EnableJobInteraction", 0) + self.__windowCloseApplication() diff --git a/cuegui/cuegui/MenuActions.py b/cuegui/cuegui/MenuActions.py index 75e88e73b..30b5c9d8e 100644 --- a/cuegui/cuegui/MenuActions.py +++ b/cuegui/cuegui/MenuActions.py @@ -373,6 +373,7 @@ def resume(self, rpcObjects=None): def kill(self, rpcObjects=None): jobs = self._getOnlyJobObjects(rpcObjects) + permissions = False if jobs: msg = ("Are you sure you want to kill these jobs?\n\n" "** Note: This will stop all running frames and " @@ -381,8 +382,17 @@ def kill(self, rpcObjects=None): if cuegui.Utils.questionBoxYesNo(self._caller, "Kill jobs?", msg, [job.data.name for job in jobs]): for job in jobs: - job.kill(reason=DEFAULT_JOB_KILL_REASON) - self.killDependents(jobs) + # check permissions + if not cuegui.Utils.isPermissible(job): + msg = cuegui.Constants.USER_INTERACTION_PERMISSIONS.format("kill", + "job(s)", + job.username()) + cuegui.Utils.showErrorMessageBox(msg) + else: + job.kill(reason=DEFAULT_JOB_KILL_REASON) + permissions = True + if permissions: + self.killDependents(jobs) self._update() def killDependents(self, jobs): @@ -453,7 +463,14 @@ def eatDead(self, rpcObjects=None): "Eat all DEAD frames in selected jobs?", [job.data.name for job in jobs]): for job in jobs: - job.eatFrames(state=[opencue.compiled_proto.job_pb2.DEAD]) + # check permissions + if not cuegui.Utils.isPermissible(job): + msg = cuegui.Constants.USER_INTERACTION_PERMISSIONS.format("eat dead", + "job", + job.username()) + cuegui.Utils.showErrorMessageBox(msg) + else: + job.eatFrames(state=[opencue.compiled_proto.job_pb2.DEAD]) self._update() autoEatOn_info = ["Enable auto eating", None, "eat"] @@ -480,12 +497,19 @@ def autoEatOff(self, rpcObjects=None): def retryDead(self, rpcObjects=None): jobs = self._getOnlyJobObjects(rpcObjects) if jobs: + # check permissions if cuegui.Utils.questionBoxYesNo(self._caller, "Confirm", "Retry all DEAD frames in selected jobs?", [job.data.name for job in jobs]): for job in jobs: - job.retryFrames( - state=[opencue.compiled_proto.job_pb2.DEAD]) + if not cuegui.Utils.isPermissible(job): + msg = cuegui.Constants.USER_INTERACTION_PERMISSIONS.format("retry dead", + "job", + job.username()) + cuegui.showErrorMessageBox(msg) + else: + job.retryFrames( + state=[opencue.compiled_proto.job_pb2.DEAD]) self._update() dropExternalDependencies_info = ["Drop External Dependencies", None, "kill"] @@ -778,48 +802,73 @@ def setTags(self, rpcObjects=None): def kill(self, rpcObjects=None): layers = self._getOnlyLayerObjects(rpcObjects) if layers: - if cuegui.Utils.questionBoxYesNo(self._caller, "Confirm", - "Kill ALL frames in selected layers?", - [layer.data.name for layer in layers]): - for layer in layers: - layer.kill(reason=DEFAULT_FRAME_KILL_REASON) - self._update() + #check permissions + if not cuegui.Utils.isPermissible(self._getSource()): + msg = cuegui.Constants.USER_INTERACTION_PERMISSIONS.format("kill", + "layers", + self._getSource().username()) + cuegui.Utils.showErrorMessageBox(msg) + else: + if cuegui.Utils.questionBoxYesNo(self._caller, "Confirm", + "Kill ALL frames in selected layers?", + [layer.data.name for layer in layers]): + for layer in layers: + layer.kill(reason=DEFAULT_FRAME_KILL_REASON) + self._update() eat_info = ["&Eat", None, "eat"] def eat(self, rpcObjects=None): layers = self._getOnlyLayerObjects(rpcObjects) if layers: - if cuegui.Utils.questionBoxYesNo(self._caller, "Confirm", - "Eat ALL frames in selected layers?", - [layer.data.name for layer in layers]): - for layer in layers: - layer.eat() - self._update() + if not cuegui.Utils.isPermissible(self._getSource()): + msg = cuegui.Constants.USER_INTERACTION_PERMISSIONS.format("eat", + "layers", + self._getSource().username()) + cuegui.Utils.showErrorMessageBox(msg) + else: + if cuegui.Utils.questionBoxYesNo(self._caller, "Confirm", + "Eat ALL frames in selected layers?", + [layer.data.name for layer in layers]): + for layer in layers: + layer.eat() + self._update() retry_info = ["&Retry", None, "retry"] def retry(self, rpcObjects=None): layers = self._getOnlyLayerObjects(rpcObjects) if layers: - if cuegui.Utils.questionBoxYesNo(self._caller, "Confirm", - "Retry ALL frames in selected layers?", - [layer.data.name for layer in layers]): - for layer in layers: - layer.retry() - self._update() + if not cuegui.Utils.isPermissible(self._getSource()): + msg = cuegui.Constants.USER_INTERACTION_PERMISSIONS.format("retry", + "layers", + self._getSource().username()) + cuegui.Utils.showErrorMessageBox(msg) + else: + if cuegui.Utils.questionBoxYesNo(self._caller, "Confirm", + "Retry ALL frames in selected layers?", + [layer.data.name for layer in layers]): + for layer in layers: + layer.retry() + self._update() retryDead_info = ["Retry dead frames", None, "retry"] def retryDead(self, rpcObjects=None): layers = self._getOnlyLayerObjects(rpcObjects) if layers: - if cuegui.Utils.questionBoxYesNo(self._caller, "Confirm", - "Retry all DEAD frames in selected layers?", - [layer.data.name for layer in layers]): - layers[-1].parent().retryFrames(layer=[layer.data.name for layer in layers], - state=[opencue.api.job_pb2.DEAD]) - self._update() + if not cuegui.Utils.isPermissible(self._getSource()): + msg = cuegui.Constants.USER_INTERACTION_PERMISSIONS.format("retry dead", + "layers", + self._getSource().username()) + cuegui.Utils.showErrorMessageBox(msg) + else: + if cuegui.Utils.questionBoxYesNo(self._caller, "Confirm", + "Retry all DEAD frames in selected layers?", + [layer.data.name for layer in layers]): + layers[-1].parent().retryFrames(layer=[layer.data.name for layer in layers], + state=[opencue.api.job_pb2.DEAD]) + self._update() markdone_info = ["Mark done", None, "markdone"] @@ -1042,10 +1091,17 @@ def retry(self, rpcObjects=None): names = [frame.data.name for frame in self._getOnlyFrameObjects(rpcObjects)] if names: job = self._getSource() - if cuegui.Utils.questionBoxYesNo( - self._caller, "Confirm", "Retry selected frames?", names): - job.retryFrames(name=names) - self._update() + # check permissions + if not cuegui.Utils.isPermissible(job): + msg = cuegui.Constants.USER_INTERACTION_PERMISSIONS.format("retry", + "frames", + job.username()) + cuegui.Utils.showErrorMessageBox(msg) + else: + if cuegui.Utils.questionBoxYesNo( + self._caller, "Confirm", "Retry selected frames?", names): + job.retryFrames(name=names) + self._update() previewMain_info = ["Preview Main", None, "previewMain"] @@ -1079,23 +1135,37 @@ def previewAovs(self, rpcObjects=None): def eat(self, rpcObjects=None): names = [frame.data.name for frame in self._getOnlyFrameObjects(rpcObjects)] if names: - if cuegui.Utils.questionBoxYesNo(self._caller, "Confirm", - "Eat selected frames?", - names): - self._getSource().eatFrames(name=names) - self._update() + #check permissions + print(self._getSource()) + if not cuegui.Utils.isPermissible(self._getSource()): + msg = cuegui.Constants.USER_INTERACTION_PERMISSIONS.format("eat", + "frames", + self._getSource().username()) + cuegui.Utils.showErrorMessageBox(msg) + else: + if cuegui.Utils.questionBoxYesNo(self._caller, "Confirm", + "Eat selected frames?", + names): + self._getSource().eatFrames(name=names) + self._update() kill_info = ["&Kill", None, "kill"] def kill(self, rpcObjects=None): names = [frame.data.name for frame in self._getOnlyFrameObjects(rpcObjects)] if names: - if cuegui.Utils.questionBoxYesNo(self._caller, "Confirm", - "Kill selected frames?", - names): - self._getSource().killFrames(reason=DEFAULT_FRAME_KILL_REASON, - name=names) - self._update() + if not cuegui.Utils.isPermissible(self._getSource(), self): + msg = cuegui.Constants.USER_INTERACTION_PERMISSIONS.format("kill", + "frames", + self._getSource().username()) + cuegui.Utils.showErrorMessageBox(msg) + else: + if cuegui.Utils.questionBoxYesNo(self._caller, "Confirm", + "Kill selected frames?", + names): + self._getSource().killFrames(reason=DEFAULT_FRAME_KILL_REASON, + name=names) + self._update() markAsWaiting_info = ["Mark as &waiting", None, "configure"] @@ -1199,47 +1269,53 @@ def eatandmarkdone(self, rpcObjects=None): frames = self._getOnlyFrameObjects(rpcObjects) if frames: frameNames = [frame.data.name for frame in frames] + #check permissions + if not cuegui.Utils.isPermissible(self._getSource(), self): + msg = cuegui.Constants.USER_INTERACTION_PERMISSIONS.format("eat and mark done", + "frames", + self._getSource().username()) + cuegui.Utils.showErrorMessageBox(msg) + else: + if cuegui.Utils.questionBoxYesNo( + self._caller, "Confirm", + "Eat and Mark done all selected frames?\n" + "(Drops any dependencies that are waiting on these frames)\n\n" + "If a frame is part of a layer that will now only contain\n" + "eaten or succeeded frames, any dependencies on the\n" + "layer will be dropped as well.", + frameNames): + + # Mark done the layers to drop their dependencies if the layer is done + + if len(frames) == 1: + # Since only a single frame selected, check if layer is only one frame + layer = opencue.api.findLayer(self._getSource().data.name, + frames[0].data.layer_name) + if layer.data.layer_stats.total_frames == 1: + # Single frame selected of single frame layer, mark done and eat it all + layer.eat() + layer.markdone() - if cuegui.Utils.questionBoxYesNo( - self._caller, "Confirm", - "Eat and Mark done all selected frames?\n" - "(Drops any dependencies that are waiting on these frames)\n\n" - "If a frame is part of a layer that will now only contain\n" - "eaten or succeeded frames, any dependencies on the\n" - "layer will be dropped as well.", - frameNames): - - # Mark done the layers to drop their dependencies if the layer is done - - if len(frames) == 1: - # Since only a single frame selected, check if layer is only one frame - layer = opencue.api.findLayer(self._getSource().data.name, - frames[0].data.layer_name) - if layer.data.layer_stats.total_frames == 1: - # Single frame selected of single frame layer, mark done and eat it all - layer.eat() - layer.markdone() + self._update() + return - self._update() - return + self._getSource().eatFrames(name=frameNames) + self._getSource().markdoneFrames(name=frameNames) - self._getSource().eatFrames(name=frameNames) - self._getSource().markdoneFrames(name=frameNames) + # Warning: The below assumes that eaten frames are desired to be markdone - # Warning: The below assumes that eaten frames are desired to be markdone - - # Wait for the markDoneFrames to be processed, then drop the dependencies on - # the layer if all frames are done. - layerNames = [frame.data.layer_name for frame in frames] - time.sleep(1) - for layer in self._getSource().getLayers(): - if layer.data.name in layerNames: - if ( - layer.data.layer_stats.eaten_frames + - layer.data.layer_stats.succeeded_frames == - layer.data.layer_stats.total_frames): - layer.markdone() - self._update() + # Wait for the markDoneFrames to be processed, then drop the dependencies on + # the layer if all frames are done. + layerNames = [frame.data.layer_name for frame in frames] + time.sleep(1) + for layer in self._getSource().getLayers(): + if layer.data.name in layerNames: + if ( + layer.data.layer_stats.eaten_frames + + layer.data.layer_stats.succeeded_frames == + layer.data.layer_stats.total_frames): + layer.markdone() + self._update() class ShowActions(AbstractActions): @@ -1672,14 +1748,21 @@ def view(self, rpcObjects=None): def kill(self, rpcObjects=None): procs = self._getOnlyProcObjects(rpcObjects) if procs: - if cuegui.Utils.questionBoxYesNo( - self._caller, "Confirm", "Kill selected frames?", - ["%s -> %s @ %s" % (proc.data.job_name, proc.data.frame_name, proc.data.name) - for proc in procs]): - for proc in procs: - self.cuebotCall(proc.kill, - "Kill Proc %s Failed" % proc.data.name) - self._update() + print(self._getSource()) + if not cuegui.Utils.isPermissible(self._getSource(), self): + msg = cuegui.Constants.USER_INTERACTION_PERMISSIONS.format("eat and mark done", + "frames", + self._getSource().username()) + cuegui.Utils.showErrorMessageBox(msg) + else: + if cuegui.Utils.questionBoxYesNo( + self._caller, "Confirm", "Kill selected frames?", + ["%s -> %s @ %s" % (proc.data.job_name, proc.data.frame_name, proc.data.name) + for proc in procs]): + for proc in procs: + self.cuebotCall(proc.kill, + "Kill Proc %s Failed" % proc.data.name) + self._update() unbook_info = ["Unbook", None, "eject"] diff --git a/cuegui/cuegui/Utils.py b/cuegui/cuegui/Utils.py index 5b88345fd..da8b596d2 100644 --- a/cuegui/cuegui/Utils.py +++ b/cuegui/cuegui/Utils.py @@ -35,6 +35,7 @@ from qtpy import QtCore from qtpy import QtGui from qtpy import QtWidgets +import getpass import six import opencue @@ -671,3 +672,25 @@ def byteConversion(amount, btype): for _ in range(n): _bytes *= 1024 return _bytes + + +def isPermissible(jobObject): + """ + Validate if the current user has the correct permissions to perform + the action + + :param userName: jobObject + :ptype userName: Opencue Job Object + :return: + """ + hasPermissions = False + # Case 1. Check if current user is the job owner + currentUser = getpass.getuser() + if currentUser.lower() == jobObject.username().lower(): + hasPermissions = True + + # Case 2. Check if "Enable not owned Job Interactions" is Enabled + if bool(int(QtGui.qApp.settings.value("EnableJobInteraction", 0))): + hasPermissions = True + + return hasPermissions From 59c6a147fb531ed70b70babfb9de7c4b6c6c35bd Mon Sep 17 00:00:00 2001 From: Diego Tavares Date: Tue, 20 Aug 2024 14:20:41 -0700 Subject: [PATCH 2/4] Remove unecessary constants from Constants.py USER_CONFIRM_RESTART and USER_INTERACTION_PERMISSIONS are only used internally on a single class each, there's no point in making them global gui constants. --- cuegui/cuegui/Constants.py | 2 - cuegui/cuegui/MainWindow.py | 7 +++- cuegui/cuegui/MenuActions.py | 75 +++++++++++++++++++----------------- 3 files changed, 44 insertions(+), 40 deletions(-) diff --git a/cuegui/cuegui/Constants.py b/cuegui/cuegui/Constants.py index b25ae0e50..13b511317 100644 --- a/cuegui/cuegui/Constants.py +++ b/cuegui/cuegui/Constants.py @@ -137,8 +137,6 @@ def __packaged_version(): EMAIL_BODY_PREFIX = __config.get('email.body_prefix') EMAIL_BODY_SUFFIX = __config.get('email.body_suffix') EMAIL_DOMAIN = __config.get('email.domain') -USER_CONFIRM_RESTART = "You must restart for this action to take effect, close window?: " -USER_INTERACTION_PERMISSIONS = "You do not have permissions to {0} {1} owned by {2}" GITHUB_CREATE_ISSUE_URL = __config.get('links.issue.create') URL_USERGUIDE = __config.get('links.user_guide') diff --git a/cuegui/cuegui/MainWindow.py b/cuegui/cuegui/MainWindow.py index 1cbb7cd6b..e9b4bc9a3 100644 --- a/cuegui/cuegui/MainWindow.py +++ b/cuegui/cuegui/MainWindow.py @@ -46,6 +46,9 @@ class MainWindow(QtWidgets.QMainWindow): """The main window of the application. Multiple windows may exist.""" + # Message to be displayed when a change requires an application restart + USER_CONFIRM_RESTART = "You must restart for this action to take effect, close window?: " + windows = [] windows_names = [] windows_titles = {} @@ -469,7 +472,7 @@ def __revertLayout(self): result = QtWidgets.QMessageBox.question( self, "Restart required ", - cuegui.Constants.USER_CONFIRM_RESTART, + MainWindow.USER_CONFIRM_RESTART, QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) if result == QtWidgets.QMessageBox.Yes: @@ -481,7 +484,7 @@ def __enableJobInteraction(self): result = QtWidgets.QMessageBox.question( self, "Job Interaction Settings ", - cuegui.Constants.USER_CONFIRM_RESTART, + MainWindow.USER_CONFIRM_RESTART, QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) if result == QtWidgets.QMessageBox.Yes: diff --git a/cuegui/cuegui/MenuActions.py b/cuegui/cuegui/MenuActions.py index 30b5c9d8e..70cddc770 100644 --- a/cuegui/cuegui/MenuActions.py +++ b/cuegui/cuegui/MenuActions.py @@ -214,6 +214,9 @@ def getText(self, title, body, default): class JobActions(AbstractActions): """Actions for jobs.""" + # Template for permission alert messages + USER_INTERACTION_PERMISSIONS = "You do not have permissions to {0} {1} owned by {2}" + def __init__(self, *args): AbstractActions.__init__(self, *args) @@ -384,9 +387,9 @@ def kill(self, rpcObjects=None): for job in jobs: # check permissions if not cuegui.Utils.isPermissible(job): - msg = cuegui.Constants.USER_INTERACTION_PERMISSIONS.format("kill", - "job(s)", - job.username()) + msg = JobActions.USER_INTERACTION_PERMISSIONS.format("kill", + "job(s)", + job.username()) cuegui.Utils.showErrorMessageBox(msg) else: job.kill(reason=DEFAULT_JOB_KILL_REASON) @@ -465,9 +468,9 @@ def eatDead(self, rpcObjects=None): for job in jobs: # check permissions if not cuegui.Utils.isPermissible(job): - msg = cuegui.Constants.USER_INTERACTION_PERMISSIONS.format("eat dead", - "job", - job.username()) + msg = JobActions.USER_INTERACTION_PERMISSIONS.format("eat dead", + "job", + job.username()) cuegui.Utils.showErrorMessageBox(msg) else: job.eatFrames(state=[opencue.compiled_proto.job_pb2.DEAD]) @@ -503,9 +506,9 @@ def retryDead(self, rpcObjects=None): [job.data.name for job in jobs]): for job in jobs: if not cuegui.Utils.isPermissible(job): - msg = cuegui.Constants.USER_INTERACTION_PERMISSIONS.format("retry dead", - "job", - job.username()) + msg = JobActions.USER_INTERACTION_PERMISSIONS.format("retry dead", + "job", + job.username()) cuegui.showErrorMessageBox(msg) else: job.retryFrames( @@ -804,9 +807,9 @@ def kill(self, rpcObjects=None): if layers: #check permissions if not cuegui.Utils.isPermissible(self._getSource()): - msg = cuegui.Constants.USER_INTERACTION_PERMISSIONS.format("kill", - "layers", - self._getSource().username()) + msg = JobActions.USER_INTERACTION_PERMISSIONS.format("kill", + "layers", + self._getSource().username()) cuegui.Utils.showErrorMessageBox(msg) else: if cuegui.Utils.questionBoxYesNo(self._caller, "Confirm", @@ -822,9 +825,9 @@ def eat(self, rpcObjects=None): layers = self._getOnlyLayerObjects(rpcObjects) if layers: if not cuegui.Utils.isPermissible(self._getSource()): - msg = cuegui.Constants.USER_INTERACTION_PERMISSIONS.format("eat", - "layers", - self._getSource().username()) + msg = JobActions.USER_INTERACTION_PERMISSIONS.format("eat", + "layers", + self._getSource().username()) cuegui.Utils.showErrorMessageBox(msg) else: if cuegui.Utils.questionBoxYesNo(self._caller, "Confirm", @@ -840,9 +843,9 @@ def retry(self, rpcObjects=None): layers = self._getOnlyLayerObjects(rpcObjects) if layers: if not cuegui.Utils.isPermissible(self._getSource()): - msg = cuegui.Constants.USER_INTERACTION_PERMISSIONS.format("retry", - "layers", - self._getSource().username()) + msg = JobActions.USER_INTERACTION_PERMISSIONS.format("retry", + "layers", + self._getSource().username()) cuegui.Utils.showErrorMessageBox(msg) else: if cuegui.Utils.questionBoxYesNo(self._caller, "Confirm", @@ -858,9 +861,9 @@ def retryDead(self, rpcObjects=None): layers = self._getOnlyLayerObjects(rpcObjects) if layers: if not cuegui.Utils.isPermissible(self._getSource()): - msg = cuegui.Constants.USER_INTERACTION_PERMISSIONS.format("retry dead", - "layers", - self._getSource().username()) + msg = JobActions.USER_INTERACTION_PERMISSIONS.format("retry dead", + "layers", + self._getSource().username()) cuegui.Utils.showErrorMessageBox(msg) else: if cuegui.Utils.questionBoxYesNo(self._caller, "Confirm", @@ -1093,9 +1096,9 @@ def retry(self, rpcObjects=None): job = self._getSource() # check permissions if not cuegui.Utils.isPermissible(job): - msg = cuegui.Constants.USER_INTERACTION_PERMISSIONS.format("retry", - "frames", - job.username()) + msg = JobActions.USER_INTERACTION_PERMISSIONS.format("retry", + "frames", + job.username()) cuegui.Utils.showErrorMessageBox(msg) else: if cuegui.Utils.questionBoxYesNo( @@ -1138,9 +1141,9 @@ def eat(self, rpcObjects=None): #check permissions print(self._getSource()) if not cuegui.Utils.isPermissible(self._getSource()): - msg = cuegui.Constants.USER_INTERACTION_PERMISSIONS.format("eat", - "frames", - self._getSource().username()) + msg = JobActions.USER_INTERACTION_PERMISSIONS.format("eat", + "frames", + self._getSource().username()) cuegui.Utils.showErrorMessageBox(msg) else: if cuegui.Utils.questionBoxYesNo(self._caller, "Confirm", @@ -1155,9 +1158,9 @@ def kill(self, rpcObjects=None): names = [frame.data.name for frame in self._getOnlyFrameObjects(rpcObjects)] if names: if not cuegui.Utils.isPermissible(self._getSource(), self): - msg = cuegui.Constants.USER_INTERACTION_PERMISSIONS.format("kill", - "frames", - self._getSource().username()) + msg = JobActions.USER_INTERACTION_PERMISSIONS.format("kill", + "frames", + self._getSource().username()) cuegui.Utils.showErrorMessageBox(msg) else: if cuegui.Utils.questionBoxYesNo(self._caller, "Confirm", @@ -1271,9 +1274,9 @@ def eatandmarkdone(self, rpcObjects=None): frameNames = [frame.data.name for frame in frames] #check permissions if not cuegui.Utils.isPermissible(self._getSource(), self): - msg = cuegui.Constants.USER_INTERACTION_PERMISSIONS.format("eat and mark done", - "frames", - self._getSource().username()) + msg = JobActions.USER_INTERACTION_PERMISSIONS.format("eat and mark done", + "frames", + self._getSource().username()) cuegui.Utils.showErrorMessageBox(msg) else: if cuegui.Utils.questionBoxYesNo( @@ -1750,9 +1753,9 @@ def kill(self, rpcObjects=None): if procs: print(self._getSource()) if not cuegui.Utils.isPermissible(self._getSource(), self): - msg = cuegui.Constants.USER_INTERACTION_PERMISSIONS.format("eat and mark done", - "frames", - self._getSource().username()) + msg = JobActions.USER_INTERACTION_PERMISSIONS.format("eat and mark done", + "frames", + self._getSource().username()) cuegui.Utils.showErrorMessageBox(msg) else: if cuegui.Utils.questionBoxYesNo( From 7b6f9aa5953f9e7bdc88d0d3964cfa1dc9fc72c8 Mon Sep 17 00:00:00 2001 From: Diego Tavares Date: Tue, 20 Aug 2024 14:53:03 -0700 Subject: [PATCH 3/4] Avoid spamming errors when interacting with multiple jobs The permission logic should collect all the blocked actions before creating the warning dialog when interacting with multiple items at the same time. --- cuegui/cuegui/MenuActions.py | 122 ++++++++++++++++++++--------------- 1 file changed, 69 insertions(+), 53 deletions(-) diff --git a/cuegui/cuegui/MenuActions.py b/cuegui/cuegui/MenuActions.py index 70cddc770..c85df27b9 100644 --- a/cuegui/cuegui/MenuActions.py +++ b/cuegui/cuegui/MenuActions.py @@ -215,7 +215,7 @@ class JobActions(AbstractActions): """Actions for jobs.""" # Template for permission alert messages - USER_INTERACTION_PERMISSIONS = "You do not have permissions to {0} {1} owned by {2}" + USER_INTERACTION_PERMISSIONS = "You do not have permissions to {0} owned by {2}" def __init__(self, *args): AbstractActions.__init__(self, *args) @@ -376,7 +376,6 @@ def resume(self, rpcObjects=None): def kill(self, rpcObjects=None): jobs = self._getOnlyJobObjects(rpcObjects) - permissions = False if jobs: msg = ("Are you sure you want to kill these jobs?\n\n" "** Note: This will stop all running frames and " @@ -384,18 +383,22 @@ def kill(self, rpcObjects=None): "The jobs will NOT be able to return once killed.") if cuegui.Utils.questionBoxYesNo(self._caller, "Kill jobs?", msg, [job.data.name for job in jobs]): + blocked_job_owners = [] + authorized_jobs = [] for job in jobs: # check permissions if not cuegui.Utils.isPermissible(job): - msg = JobActions.USER_INTERACTION_PERMISSIONS.format("kill", - "job(s)", - job.username()) - cuegui.Utils.showErrorMessageBox(msg) + blocked_job_owners.append(job.username()) else: job.kill(reason=DEFAULT_JOB_KILL_REASON) - permissions = True - if permissions: - self.killDependents(jobs) + authorized_jobs.append(job) + if authorized_jobs: + self.killDependents(authorized_jobs) + if blocked_job_owners: + cuegui.Utils.showErrorMessageBox( + JobActions.USER_INTERACTION_PERMISSIONS.format( + "kill some of the selected jobs", + ", ".join(blocked_job_owners))) self._update() def killDependents(self, jobs): @@ -465,15 +468,18 @@ def eatDead(self, rpcObjects=None): if cuegui.Utils.questionBoxYesNo(self._caller, "Confirm", "Eat all DEAD frames in selected jobs?", [job.data.name for job in jobs]): + blocked_job_owners = [] for job in jobs: # check permissions if not cuegui.Utils.isPermissible(job): - msg = JobActions.USER_INTERACTION_PERMISSIONS.format("eat dead", - "job", - job.username()) - cuegui.Utils.showErrorMessageBox(msg) + blocked_job_owners.append(job.username()) else: job.eatFrames(state=[opencue.compiled_proto.job_pb2.DEAD]) + if blocked_job_owners: + cuegui.Utils.showErrorMessageBox( + JobActions.USER_INTERACTION_PERMISSIONS.format( + "eat dead for some of the selected jobs", + ", ".join(blocked_job_owners))) self._update() autoEatOn_info = ["Enable auto eating", None, "eat"] @@ -504,15 +510,18 @@ def retryDead(self, rpcObjects=None): if cuegui.Utils.questionBoxYesNo(self._caller, "Confirm", "Retry all DEAD frames in selected jobs?", [job.data.name for job in jobs]): + blocked_job_owners = [] for job in jobs: if not cuegui.Utils.isPermissible(job): - msg = JobActions.USER_INTERACTION_PERMISSIONS.format("retry dead", - "job", - job.username()) - cuegui.showErrorMessageBox(msg) + blocked_job_owners.append(job.username()) else: job.retryFrames( state=[opencue.compiled_proto.job_pb2.DEAD]) + if blocked_job_owners: + cuegui.Utils.showErrorMessageBox( + JobActions.USER_INTERACTION_PERMISSIONS.format( + "retry dead for some of the selected jobs", + ", ".join(blocked_job_owners))) self._update() dropExternalDependencies_info = ["Drop External Dependencies", None, "kill"] @@ -807,10 +816,10 @@ def kill(self, rpcObjects=None): if layers: #check permissions if not cuegui.Utils.isPermissible(self._getSource()): - msg = JobActions.USER_INTERACTION_PERMISSIONS.format("kill", - "layers", - self._getSource().username()) - cuegui.Utils.showErrorMessageBox(msg) + cuegui.Utils.showErrorMessageBox( + JobActions.USER_INTERACTION_PERMISSIONS.format( + "kill layers", + self._getSource().username())) else: if cuegui.Utils.questionBoxYesNo(self._caller, "Confirm", "Kill ALL frames in selected layers?", @@ -825,10 +834,11 @@ def eat(self, rpcObjects=None): layers = self._getOnlyLayerObjects(rpcObjects) if layers: if not cuegui.Utils.isPermissible(self._getSource()): - msg = JobActions.USER_INTERACTION_PERMISSIONS.format("eat", - "layers", - self._getSource().username()) - cuegui.Utils.showErrorMessageBox(msg) + cuegui.Utils.showErrorMessageBox( + JobActions.USER_INTERACTION_PERMISSIONS.format( + "eat layers", + self._getSource().username()) + ) else: if cuegui.Utils.questionBoxYesNo(self._caller, "Confirm", "Eat ALL frames in selected layers?", @@ -843,10 +853,11 @@ def retry(self, rpcObjects=None): layers = self._getOnlyLayerObjects(rpcObjects) if layers: if not cuegui.Utils.isPermissible(self._getSource()): - msg = JobActions.USER_INTERACTION_PERMISSIONS.format("retry", - "layers", - self._getSource().username()) - cuegui.Utils.showErrorMessageBox(msg) + cuegui.Utils.showErrorMessageBox( + JobActions.USER_INTERACTION_PERMISSIONS.format( + "retry layers", + self._getSource().username()) + ) else: if cuegui.Utils.questionBoxYesNo(self._caller, "Confirm", "Retry ALL frames in selected layers?", @@ -861,10 +872,11 @@ def retryDead(self, rpcObjects=None): layers = self._getOnlyLayerObjects(rpcObjects) if layers: if not cuegui.Utils.isPermissible(self._getSource()): - msg = JobActions.USER_INTERACTION_PERMISSIONS.format("retry dead", - "layers", - self._getSource().username()) - cuegui.Utils.showErrorMessageBox(msg) + cuegui.Utils.showErrorMessageBox( + JobActions.USER_INTERACTION_PERMISSIONS.format( + "retry dead layers", + self._getSource().username()) + ) else: if cuegui.Utils.questionBoxYesNo(self._caller, "Confirm", "Retry all DEAD frames in selected layers?", @@ -1096,10 +1108,11 @@ def retry(self, rpcObjects=None): job = self._getSource() # check permissions if not cuegui.Utils.isPermissible(job): - msg = JobActions.USER_INTERACTION_PERMISSIONS.format("retry", - "frames", - job.username()) - cuegui.Utils.showErrorMessageBox(msg) + cuegui.Utils.showErrorMessageBox( + JobActions.USER_INTERACTION_PERMISSIONS.format( + "retry frames", + job.username()) + ) else: if cuegui.Utils.questionBoxYesNo( self._caller, "Confirm", "Retry selected frames?", names): @@ -1141,10 +1154,11 @@ def eat(self, rpcObjects=None): #check permissions print(self._getSource()) if not cuegui.Utils.isPermissible(self._getSource()): - msg = JobActions.USER_INTERACTION_PERMISSIONS.format("eat", - "frames", - self._getSource().username()) - cuegui.Utils.showErrorMessageBox(msg) + cuegui.Utils.showErrorMessageBox( + JobActions.USER_INTERACTION_PERMISSIONS.format( + "eat frames", + self._getSource().username()) + ) else: if cuegui.Utils.questionBoxYesNo(self._caller, "Confirm", "Eat selected frames?", @@ -1158,10 +1172,10 @@ def kill(self, rpcObjects=None): names = [frame.data.name for frame in self._getOnlyFrameObjects(rpcObjects)] if names: if not cuegui.Utils.isPermissible(self._getSource(), self): - msg = JobActions.USER_INTERACTION_PERMISSIONS.format("kill", - "frames", - self._getSource().username()) - cuegui.Utils.showErrorMessageBox(msg) + cuegui.Utils.showErrorMessageBox( + JobActions.USER_INTERACTION_PERMISSIONS.format( + "kill frames", + self._getSource().username())) else: if cuegui.Utils.questionBoxYesNo(self._caller, "Confirm", "Kill selected frames?", @@ -1274,10 +1288,11 @@ def eatandmarkdone(self, rpcObjects=None): frameNames = [frame.data.name for frame in frames] #check permissions if not cuegui.Utils.isPermissible(self._getSource(), self): - msg = JobActions.USER_INTERACTION_PERMISSIONS.format("eat and mark done", - "frames", - self._getSource().username()) - cuegui.Utils.showErrorMessageBox(msg) + cuegui.Utils.showErrorMessageBox( + JobActions.USER_INTERACTION_PERMISSIONS.format( + "eat and mark done frames", + self._getSource().username()) + ) else: if cuegui.Utils.questionBoxYesNo( self._caller, "Confirm", @@ -1753,10 +1768,11 @@ def kill(self, rpcObjects=None): if procs: print(self._getSource()) if not cuegui.Utils.isPermissible(self._getSource(), self): - msg = JobActions.USER_INTERACTION_PERMISSIONS.format("eat and mark done", - "frames", - self._getSource().username()) - cuegui.Utils.showErrorMessageBox(msg) + cuegui.Utils.showErrorMessageBox( + JobActions.USER_INTERACTION_PERMISSIONS.format( + "eat and mark done frames", + self._getSource().username()) + ) else: if cuegui.Utils.questionBoxYesNo( self._caller, "Confirm", "Kill selected frames?", From a61b939499d443282ba0a5844bf81456d1c416bb Mon Sep 17 00:00:00 2001 From: Diego Tavares Date: Tue, 20 Aug 2024 15:13:35 -0700 Subject: [PATCH 4/4] Fix lint and unit-tests --- cuegui/cuegui/MainWindow.py | 9 +- cuegui/cuegui/MenuActions.py | 147 +++++++++++++++++------------- cuegui/cuegui/Utils.py | 14 +-- cuegui/tests/MenuActions_tests.py | 64 ++++++++----- 4 files changed, 137 insertions(+), 97 deletions(-) diff --git a/cuegui/cuegui/MainWindow.py b/cuegui/cuegui/MainWindow.py index e9b4bc9a3..353c3ca6a 100644 --- a/cuegui/cuegui/MainWindow.py +++ b/cuegui/cuegui/MainWindow.py @@ -27,6 +27,7 @@ from builtins import range import sys import time +import yaml from qtpy import QtCore from qtpy import QtGui @@ -71,7 +72,7 @@ def __init__(self, app_name, app_version, window_name, parent = None): self.name = window_name else: self.name = self.windows_names[0] - self.__isEnabled = bool(int(QtGui.qApp.settings.value("EnableJobInteraction", 0))) + self.__isEnabled = yaml.safe_load(self.app.settings.value("EnableJobInteraction", "False")) # Provides a location for widgets to the right of the menu menuLayout = QtWidgets.QHBoxLayout() @@ -201,14 +202,16 @@ def __createMenus(self): if self.__isEnabled is False: # Menu Bar: File -> Enable Job Interaction - enableJobInteraction = QtWidgets.QAction(QtGui.QIcon('icons/exit.png'), '&Enable Job Interaction', self) + enableJobInteraction = QtWidgets.QAction(QtGui.QIcon('icons/exit.png'), + '&Enable Job Interaction', self) enableJobInteraction.setStatusTip('Enable Job Interaction') enableJobInteraction.triggered.connect(self.__enableJobInteraction) self.fileMenu.addAction(enableJobInteraction) # allow user to disable the job interaction else: # Menu Bar: File -> Disable Job Interaction - enableJobInteraction = QtWidgets.QAction(QtGui.QIcon('icons/exit.png'), '&Disable Job Interaction', self) + enableJobInteraction = QtWidgets.QAction(QtGui.QIcon('icons/exit.png'), + '&Disable Job Interaction', self) enableJobInteraction.setStatusTip('Disable Job Interaction') enableJobInteraction.triggered.connect(self.__enableJobInteraction) self.fileMenu.addAction(enableJobInteraction) diff --git a/cuegui/cuegui/MenuActions.py b/cuegui/cuegui/MenuActions.py index c85df27b9..961561871 100644 --- a/cuegui/cuegui/MenuActions.py +++ b/cuegui/cuegui/MenuActions.py @@ -79,6 +79,11 @@ class AbstractActions(object): __iconCache = {} + # Template for permission alert messages + USER_INTERACTION_PERMISSIONS = "You do not have permissions to {0} owned by {1}" \ + "\n\nJob actions can still be enabled at File > Enable Job Interaction," \ + " but caution is advised." + def __init__(self, caller, updateCallable, selectedRpcObjectsCallable, sourceCallable): self._caller = caller self.__selectedRpcObjects = selectedRpcObjectsCallable @@ -214,9 +219,6 @@ def getText(self, title, body, default): class JobActions(AbstractActions): """Actions for jobs.""" - # Template for permission alert messages - USER_INTERACTION_PERMISSIONS = "You do not have permissions to {0} owned by {2}" - def __init__(self, *args): AbstractActions.__init__(self, *args) @@ -396,7 +398,7 @@ def kill(self, rpcObjects=None): self.killDependents(authorized_jobs) if blocked_job_owners: cuegui.Utils.showErrorMessageBox( - JobActions.USER_INTERACTION_PERMISSIONS.format( + AbstractActions.USER_INTERACTION_PERMISSIONS.format( "kill some of the selected jobs", ", ".join(blocked_job_owners))) self._update() @@ -477,7 +479,7 @@ def eatDead(self, rpcObjects=None): job.eatFrames(state=[opencue.compiled_proto.job_pb2.DEAD]) if blocked_job_owners: cuegui.Utils.showErrorMessageBox( - JobActions.USER_INTERACTION_PERMISSIONS.format( + AbstractActions.USER_INTERACTION_PERMISSIONS.format( "eat dead for some of the selected jobs", ", ".join(blocked_job_owners))) self._update() @@ -487,9 +489,18 @@ def eatDead(self, rpcObjects=None): def autoEatOn(self, rpcObjects=None): jobs = self._getOnlyJobObjects(rpcObjects) if jobs: + blocked_job_owners = [] for job in jobs: - job.setAutoEat(True) - job.eatFrames(state=[opencue.compiled_proto.job_pb2.DEAD]) + if not cuegui.Utils.isPermissible(job): + blocked_job_owners.append(job.username()) + else: + job.setAutoEat(True) + job.eatFrames(state=[opencue.compiled_proto.job_pb2.DEAD]) + if blocked_job_owners: + cuegui.Utils.showErrorMessageBox( + AbstractActions.USER_INTERACTION_PERMISSIONS.format( + "enable auto eating frames", + ", ".join(blocked_job_owners))) self._update() autoEatOff_info = ["Disable auto eating", None, "eat"] @@ -497,8 +508,17 @@ def autoEatOn(self, rpcObjects=None): def autoEatOff(self, rpcObjects=None): jobs = self._getOnlyJobObjects(rpcObjects) if jobs: + blocked_job_owners = [] for job in jobs: - job.setAutoEat(False) + if not cuegui.Utils.isPermissible(job): + blocked_job_owners.append(job.username()) + else: + job.setAutoEat(False) + if blocked_job_owners: + cuegui.Utils.showErrorMessageBox( + AbstractActions.USER_INTERACTION_PERMISSIONS.format( + "disable auto eating frames", + ", ".join(blocked_job_owners))) self._update() retryDead_info = ["Retry dead frames", None, "retry"] @@ -519,7 +539,7 @@ def retryDead(self, rpcObjects=None): state=[opencue.compiled_proto.job_pb2.DEAD]) if blocked_job_owners: cuegui.Utils.showErrorMessageBox( - JobActions.USER_INTERACTION_PERMISSIONS.format( + AbstractActions.USER_INTERACTION_PERMISSIONS.format( "retry dead for some of the selected jobs", ", ".join(blocked_job_owners))) self._update() @@ -817,7 +837,7 @@ def kill(self, rpcObjects=None): #check permissions if not cuegui.Utils.isPermissible(self._getSource()): cuegui.Utils.showErrorMessageBox( - JobActions.USER_INTERACTION_PERMISSIONS.format( + AbstractActions.USER_INTERACTION_PERMISSIONS.format( "kill layers", self._getSource().username())) else: @@ -835,7 +855,7 @@ def eat(self, rpcObjects=None): if layers: if not cuegui.Utils.isPermissible(self._getSource()): cuegui.Utils.showErrorMessageBox( - JobActions.USER_INTERACTION_PERMISSIONS.format( + AbstractActions.USER_INTERACTION_PERMISSIONS.format( "eat layers", self._getSource().username()) ) @@ -854,7 +874,7 @@ def retry(self, rpcObjects=None): if layers: if not cuegui.Utils.isPermissible(self._getSource()): cuegui.Utils.showErrorMessageBox( - JobActions.USER_INTERACTION_PERMISSIONS.format( + AbstractActions.USER_INTERACTION_PERMISSIONS.format( "retry layers", self._getSource().username()) ) @@ -873,7 +893,7 @@ def retryDead(self, rpcObjects=None): if layers: if not cuegui.Utils.isPermissible(self._getSource()): cuegui.Utils.showErrorMessageBox( - JobActions.USER_INTERACTION_PERMISSIONS.format( + AbstractActions.USER_INTERACTION_PERMISSIONS.format( "retry dead layers", self._getSource().username()) ) @@ -1109,7 +1129,7 @@ def retry(self, rpcObjects=None): # check permissions if not cuegui.Utils.isPermissible(job): cuegui.Utils.showErrorMessageBox( - JobActions.USER_INTERACTION_PERMISSIONS.format( + AbstractActions.USER_INTERACTION_PERMISSIONS.format( "retry frames", job.username()) ) @@ -1151,11 +1171,9 @@ def previewAovs(self, rpcObjects=None): def eat(self, rpcObjects=None): names = [frame.data.name for frame in self._getOnlyFrameObjects(rpcObjects)] if names: - #check permissions - print(self._getSource()) if not cuegui.Utils.isPermissible(self._getSource()): cuegui.Utils.showErrorMessageBox( - JobActions.USER_INTERACTION_PERMISSIONS.format( + AbstractActions.USER_INTERACTION_PERMISSIONS.format( "eat frames", self._getSource().username()) ) @@ -1173,7 +1191,7 @@ def kill(self, rpcObjects=None): if names: if not cuegui.Utils.isPermissible(self._getSource(), self): cuegui.Utils.showErrorMessageBox( - JobActions.USER_INTERACTION_PERMISSIONS.format( + AbstractActions.USER_INTERACTION_PERMISSIONS.format( "kill frames", self._getSource().username())) else: @@ -1289,51 +1307,52 @@ def eatandmarkdone(self, rpcObjects=None): #check permissions if not cuegui.Utils.isPermissible(self._getSource(), self): cuegui.Utils.showErrorMessageBox( - JobActions.USER_INTERACTION_PERMISSIONS.format( + AbstractActions.USER_INTERACTION_PERMISSIONS.format( "eat and mark done frames", self._getSource().username()) ) - else: - if cuegui.Utils.questionBoxYesNo( - self._caller, "Confirm", - "Eat and Mark done all selected frames?\n" - "(Drops any dependencies that are waiting on these frames)\n\n" - "If a frame is part of a layer that will now only contain\n" - "eaten or succeeded frames, any dependencies on the\n" - "layer will be dropped as well.", - frameNames): - - # Mark done the layers to drop their dependencies if the layer is done - - if len(frames) == 1: - # Since only a single frame selected, check if layer is only one frame - layer = opencue.api.findLayer(self._getSource().data.name, - frames[0].data.layer_name) - if layer.data.layer_stats.total_frames == 1: - # Single frame selected of single frame layer, mark done and eat it all - layer.eat() - layer.markdone() - - self._update() - return - - self._getSource().eatFrames(name=frameNames) - self._getSource().markdoneFrames(name=frameNames) - - # Warning: The below assumes that eaten frames are desired to be markdone - - # Wait for the markDoneFrames to be processed, then drop the dependencies on - # the layer if all frames are done. - layerNames = [frame.data.layer_name for frame in frames] - time.sleep(1) - for layer in self._getSource().getLayers(): - if layer.data.name in layerNames: - if ( - layer.data.layer_stats.eaten_frames + - layer.data.layer_stats.succeeded_frames == - layer.data.layer_stats.total_frames): - layer.markdone() + return + if not cuegui.Utils.questionBoxYesNo( + self._caller, "Confirm", + "Eat and Mark done all selected frames?\n" + "(Drops any dependencies that are waiting on these frames)\n\n" + "If a frame is part of a layer that will now only contain\n" + "eaten or succeeded frames, any dependencies on the\n" + "layer will be dropped as well.", + frameNames): + return + + # Mark done the layers to drop their dependencies if the layer is done + + if len(frames) == 1: + # Since only a single frame selected, check if layer is only one frame + layer = opencue.api.findLayer(self._getSource().data.name, + frames[0].data.layer_name) + if layer.data.layer_stats.total_frames == 1: + # Single frame selected of single frame layer, mark done and eat it all + layer.eat() + layer.markdone() + self._update() + return + + self._getSource().eatFrames(name=frameNames) + self._getSource().markdoneFrames(name=frameNames) + + # Warning: The below assumes that eaten frames are desired to be markdone + + # Wait for the markDoneFrames to be processed, then drop the dependencies on + # the layer if all frames are done. + layerNames = [frame.data.layer_name for frame in frames] + time.sleep(1) + for layer in self._getSource().getLayers(): + if layer.data.name in layerNames: + if ( + layer.data.layer_stats.eaten_frames + + layer.data.layer_stats.succeeded_frames == + layer.data.layer_stats.total_frames): + layer.markdone() + self._update() class ShowActions(AbstractActions): @@ -1766,17 +1785,19 @@ def view(self, rpcObjects=None): def kill(self, rpcObjects=None): procs = self._getOnlyProcObjects(rpcObjects) if procs: - print(self._getSource()) - if not cuegui.Utils.isPermissible(self._getSource(), self): + if not cuegui.Utils.isPermissible(self._getSource()): cuegui.Utils.showErrorMessageBox( - JobActions.USER_INTERACTION_PERMISSIONS.format( + AbstractActions.USER_INTERACTION_PERMISSIONS.format( "eat and mark done frames", self._getSource().username()) ) else: if cuegui.Utils.questionBoxYesNo( self._caller, "Confirm", "Kill selected frames?", - ["%s -> %s @ %s" % (proc.data.job_name, proc.data.frame_name, proc.data.name) + ["%s -> %s @ %s" % ( + proc.data.job_name, + proc.data.frame_name, + proc.data.name) for proc in procs]): for proc in procs: self.cuebotCall(proc.kill, diff --git a/cuegui/cuegui/Utils.py b/cuegui/cuegui/Utils.py index da8b596d2..eda9509c2 100644 --- a/cuegui/cuegui/Utils.py +++ b/cuegui/cuegui/Utils.py @@ -31,11 +31,11 @@ import time import traceback import webbrowser +import yaml from qtpy import QtCore from qtpy import QtGui from qtpy import QtWidgets -import getpass import six import opencue @@ -683,14 +683,10 @@ def isPermissible(jobObject): :ptype userName: Opencue Job Object :return: """ - hasPermissions = False - # Case 1. Check if current user is the job owner + # Read cached setting from user config file + hasPermissions = yaml.safe_load(cuegui.app().settings.value("EnableJobInteraction", "False")) + # If not set by default, check if current user is the job owner currentUser = getpass.getuser() - if currentUser.lower() == jobObject.username().lower(): - hasPermissions = True - - # Case 2. Check if "Enable not owned Job Interactions" is Enabled - if bool(int(QtGui.qApp.settings.value("EnableJobInteraction", 0))): + if not hasPermissions and currentUser.lower() == jobObject.username().lower(): hasPermissions = True - return hasPermissions diff --git a/cuegui/tests/MenuActions_tests.py b/cuegui/tests/MenuActions_tests.py index 7c116b62c..9bf3a74ed 100644 --- a/cuegui/tests/MenuActions_tests.py +++ b/cuegui/tests/MenuActions_tests.py @@ -230,7 +230,8 @@ def test_resume(self): job.resume.assert_called() @mock.patch('cuegui.Utils.questionBoxYesNo', return_value=True) - def test_kill(self, yesNoMock): + @mock.patch('cuegui.Utils.isPermissible', return_value=True) + def test_kill(self, isPermissibleMock, yesNoMock): job = opencue.wrappers.job.Job(opencue.compiled_proto.job_pb2.Job(name='job-name')) job.kill = mock.Mock() job.getWhatDependsOnThis = mock.Mock() @@ -241,7 +242,8 @@ def test_kill(self, yesNoMock): job.kill.assert_called() @mock.patch('cuegui.Utils.questionBoxYesNo', return_value=False) - def test_killCanceled(self, yesNoMock): + @mock.patch('cuegui.Utils.isPermissible', return_value=True) + def test_killCanceled(self, isPermissibleMock, yesNoMock): job = opencue.wrappers.job.Job(opencue.compiled_proto.job_pb2.Job(name='job-name')) job.kill = mock.Mock() @@ -250,7 +252,8 @@ def test_killCanceled(self, yesNoMock): job.kill.assert_not_called() @mock.patch('cuegui.Utils.questionBoxYesNo', return_value=True) - def test_eatDead(self, yesNoMock): + @mock.patch('cuegui.Utils.isPermissible', return_value=True) + def test_eatDead(self, isPermissibleMock, yesNoMock): job = opencue.wrappers.job.Job(opencue.compiled_proto.job_pb2.Job(name='job-name')) job.eatFrames = mock.Mock() @@ -259,7 +262,8 @@ def test_eatDead(self, yesNoMock): job.eatFrames.assert_called_with(state=[opencue.compiled_proto.job_pb2.DEAD]) @mock.patch('cuegui.Utils.questionBoxYesNo', return_value=False) - def test_eatDeadCanceled(self, yesNoMock): + @mock.patch('cuegui.Utils.isPermissible', return_value=True) + def test_eatDeadCanceled(self, isPermissibleMock, yesNoMock): job = opencue.wrappers.job.Job(opencue.compiled_proto.job_pb2.Job(name='job-name')) job.eatFrames = mock.Mock() @@ -267,7 +271,8 @@ def test_eatDeadCanceled(self, yesNoMock): job.eatFrames.assert_not_called() - def test_autoEatOn(self): + @mock.patch('cuegui.Utils.isPermissible', return_value=True) + def test_autoEatOn(self, isPermissibleMock): job = opencue.wrappers.job.Job(opencue.compiled_proto.job_pb2.Job(name='job-name')) job.setAutoEat = mock.Mock() job.eatFrames = mock.Mock() @@ -277,7 +282,8 @@ def test_autoEatOn(self): job.setAutoEat.assert_called_with(True) job.eatFrames.assert_called_with(state=[opencue.compiled_proto.job_pb2.DEAD]) - def test_autoEatOff(self): + @mock.patch('cuegui.Utils.isPermissible', return_value=True) + def test_autoEatOff(self, isPermissibleMock): job = opencue.wrappers.job.Job(opencue.compiled_proto.job_pb2.Job(name='job-name')) job.setAutoEat = mock.Mock() @@ -286,7 +292,8 @@ def test_autoEatOff(self): job.setAutoEat.assert_called_with(False) @mock.patch('cuegui.Utils.questionBoxYesNo', return_value=True) - def test_retryDead(self, yesNoMock): + @mock.patch('cuegui.Utils.isPermissible', return_value=True) + def test_retryDead(self, isPermissibleMock, yesNoMock): job = opencue.wrappers.job.Job(opencue.compiled_proto.job_pb2.Job(name='job-name')) job.retryFrames = mock.Mock() @@ -295,7 +302,8 @@ def test_retryDead(self, yesNoMock): job.retryFrames.assert_called_with(state=[opencue.compiled_proto.job_pb2.DEAD]) @mock.patch('cuegui.Utils.questionBoxYesNo', return_value=False) - def test_retryDeadCanceled(self, yesNoMock): + @mock.patch('cuegui.Utils.isPermissible', return_value=True) + def test_retryDeadCanceled(self, isPermissibleMock, yesNoMock): job = opencue.wrappers.job.Job(opencue.compiled_proto.job_pb2.Job(name='job-name')) job.retryFrames = mock.Mock() @@ -669,7 +677,8 @@ def test_setTags(self, layerTagsDialogMock): @mock.patch.object(opencue.wrappers.layer.Layer, 'kill') @mock.patch('cuegui.Utils.questionBoxYesNo', return_value=True) - def test_kill(self, yesNoMock, killMock): + @mock.patch('cuegui.Utils.isPermissible', return_value=True) + def test_kill(self, isPermissibleMock, yesNoMock, killMock): layer = opencue.wrappers.layer.Layer( opencue.compiled_proto.job_pb2.Layer(name='arbitrary-name')) @@ -679,7 +688,8 @@ def test_kill(self, yesNoMock, killMock): @mock.patch.object(opencue.wrappers.layer.Layer, 'kill') @mock.patch('cuegui.Utils.questionBoxYesNo', return_value=False) - def test_killCanceled(self, yesNoMock, killMock): + @mock.patch('cuegui.Utils.isPermissible', return_value=True) + def test_killCanceled(self, isPermissibleMock, yesNoMock, killMock): layer = opencue.wrappers.layer.Layer( opencue.compiled_proto.job_pb2.Layer(name='arbitrary-name')) @@ -689,7 +699,8 @@ def test_killCanceled(self, yesNoMock, killMock): @mock.patch.object(opencue.wrappers.layer.Layer, 'eat') @mock.patch('cuegui.Utils.questionBoxYesNo', return_value=True) - def test_eat(self, yesNoMock, eatMock): + @mock.patch('cuegui.Utils.isPermissible', return_value=True) + def test_eat(self, isPermissibleMock, yesNoMock, eatMock): layer = opencue.wrappers.layer.Layer( opencue.compiled_proto.job_pb2.Layer(name='arbitrary-name')) @@ -699,7 +710,8 @@ def test_eat(self, yesNoMock, eatMock): @mock.patch.object(opencue.wrappers.layer.Layer, 'eat') @mock.patch('cuegui.Utils.questionBoxYesNo', return_value=False) - def test_eatCanceled(self, yesNoMock, eatMock): + @mock.patch('cuegui.Utils.isPermissible', return_value=True) + def test_eatCanceled(self, isPermissibleMock, yesNoMock, eatMock): layer = opencue.wrappers.layer.Layer( opencue.compiled_proto.job_pb2.Layer(name='arbitrary-name')) @@ -709,7 +721,8 @@ def test_eatCanceled(self, yesNoMock, eatMock): @mock.patch.object(opencue.wrappers.layer.Layer, 'retry', autospec=True) @mock.patch('cuegui.Utils.questionBoxYesNo', return_value=True) - def test_retry(self, yesNoMock, retryMock): + @mock.patch('cuegui.Utils.isPermissible', return_value=True) + def test_retry(self, isPermissibleMock, yesNoMock, retryMock): layer = opencue.wrappers.layer.Layer( opencue.compiled_proto.job_pb2.Layer(name='arbitrary-name')) @@ -719,7 +732,8 @@ def test_retry(self, yesNoMock, retryMock): @mock.patch.object(opencue.wrappers.layer.Layer, 'retry') @mock.patch('cuegui.Utils.questionBoxYesNo', return_value=False) - def test_retryCanceled(self, yesNoMock, retryMock): + @mock.patch('cuegui.Utils.isPermissible', return_value=True) + def test_retryCanceled(self, isPermissibleMock, yesNoMock, retryMock): layer = opencue.wrappers.layer.Layer( opencue.compiled_proto.job_pb2.Layer(name='arbitrary-name')) @@ -728,7 +742,8 @@ def test_retryCanceled(self, yesNoMock, retryMock): retryMock.assert_not_called() @mock.patch('cuegui.Utils.questionBoxYesNo', return_value=True) - def test_retryDead(self, yesNoMock): + @mock.patch('cuegui.Utils.isPermissible', return_value=True) + def test_retryDead(self, isPermissibleMock, yesNoMock): layer_name = 'arbitrary-name' layer = opencue.wrappers.layer.Layer( opencue.compiled_proto.job_pb2.Layer(name=layer_name)) @@ -914,7 +929,8 @@ def test_getWhatDependsOnThis(self): self.frame_actions.getWhatDependsOnThis(rpcObjects=[frame]) @mock.patch('cuegui.Utils.questionBoxYesNo', return_value=True) - def test_retry(self, yesNoMock): + @mock.patch('cuegui.Utils.isPermissible', return_value=True) + def test_retry(self, isPermissibleMock, yesNoMock): frame_name = 'arbitrary-frame-name' frame = opencue.wrappers.frame.Frame(opencue.compiled_proto.job_pb2.Frame(name=frame_name)) @@ -943,7 +959,8 @@ def test_previewAovs(self, previewProcessorDialogMock): previewProcessorDialogMock.return_value.exec_.assert_called() @mock.patch('cuegui.Utils.questionBoxYesNo', return_value=True) - def test_eat(self, yesNoMock): + @mock.patch('cuegui.Utils.isPermissible', return_value=True) + def test_eat(self, isPermissibleMock, yesNoMock): frame_name = 'arbitrary-frame-name' frame = opencue.wrappers.frame.Frame(opencue.compiled_proto.job_pb2.Frame(name=frame_name)) @@ -952,7 +969,8 @@ def test_eat(self, yesNoMock): self.job.eatFrames.assert_called_with(name=[frame_name]) @mock.patch('cuegui.Utils.questionBoxYesNo', return_value=True) - def test_kill(self, yesNoMock): + @mock.patch('cuegui.Utils.isPermissible', return_value=True) + def test_kill(self, isPermissibleMock, yesNoMock): frame_name = 'arbitrary-frame-name' frame = opencue.wrappers.frame.Frame(opencue.compiled_proto.job_pb2.Frame(name=frame_name)) @@ -1019,7 +1037,8 @@ def test_copyLogFileName(self, getFrameLogFileMock, clipboardMock): @mock.patch.object(opencue.wrappers.layer.Layer, 'markdone', autospec=True) @mock.patch('cuegui.Utils.questionBoxYesNo', return_value=True) - def test_eatandmarkdone(self, yesNoMock, markdoneMock): + @mock.patch('cuegui.Utils.isPermissible', return_value=True) + def test_eatandmarkdone(self, isPermissibleMock, yesNoMock, markdoneMock): layer_name = 'layer-name' frames = [ opencue.wrappers.frame.Frame( @@ -1339,7 +1358,7 @@ def setUp(self): self.app = test_utils.createApplication() self.widgetMock = mock.Mock() self.proc_actions = cuegui.MenuActions.ProcActions( - self.widgetMock, mock.Mock(), None, None) + self.widgetMock, mock.Mock(), mock.Mock(), mock.Mock()) @mock.patch('opencue.api.findJob') def test_view(self, findJobMock): @@ -1353,8 +1372,9 @@ def test_view(self, findJobMock): self.app.view_object.emit.assert_called_once_with(job) - @mock.patch('cuegui.Utils.questionBoxYesNo', new=mock.Mock(return_value=True)) - def test_kill(self): + @mock.patch('cuegui.Utils.questionBoxYesNo', return_value=True) + @mock.patch('cuegui.Utils.isPermissible', return_value=True) + def test_kill(self, isPermissibleMock, yesNoMock): proc = opencue.wrappers.proc.Proc(opencue.compiled_proto.host_pb2.Proc()) proc.kill = mock.MagicMock()