From d2cb67850a5b490403a328ae38ac93010880f48b Mon Sep 17 00:00:00 2001 From: Ben Souchet Date: Wed, 11 Sep 2024 14:57:44 +0200 Subject: [PATCH] Ftrack Task Status Update: Rework of the task status change + cleanup --- openpype/hosts/houdini/api/pipeline.py | 1 + openpype/hosts/nuke/api/lib.py | 2 + openpype/hosts/nuke/api/pipeline.py | 18 +- openpype/modules/ftrack/ftrack_module.py | 136 ++++++++++++++- .../launch_hooks/post_ftrack_changes.py | 165 ------------------ .../modules/ftrack/lib/ftrack_base_handler.py | 4 +- .../defaults/project_settings/ftrack.json | 53 +++--- .../schema_project_ftrack.json | 104 +++++------ 8 files changed, 228 insertions(+), 255 deletions(-) delete mode 100644 openpype/modules/ftrack/launch_hooks/post_ftrack_changes.py diff --git a/openpype/hosts/houdini/api/pipeline.py b/openpype/hosts/houdini/api/pipeline.py index d0f45c36b5b..f9b5614dd63 100644 --- a/openpype/hosts/houdini/api/pipeline.py +++ b/openpype/hosts/houdini/api/pipeline.py @@ -186,6 +186,7 @@ def on_file_event_callback(event): emit_event("open") elif event == hou.hipFileEventType.AfterSave: emit_event("save") + emit_event("after.save") elif event == hou.hipFileEventType.BeforeSave: emit_event("before.save") elif event == hou.hipFileEventType.AfterClear: diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index cdefd05c114..b4d642c8ad6 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2600,12 +2600,14 @@ def make_format_string(self, **kwargs): ) def set_context_settings(self): + os.environ["OP_NUKE_SKIP_SAVE_EVENT"] = "True" # replace reset resolution from avalon core to pype's self.reset_resolution() # replace reset resolution from avalon core to pype's self.reset_frame_range_handles() # add colorspace menu item self.set_colorspace() + del os.environ["OP_NUKE_SKIP_SAVE_EVENT"] def set_favorites(self): from .utils import set_context_favorites diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py index 2cff55030d1..f59ea64100d 100644 --- a/openpype/hosts/nuke/api/pipeline.py +++ b/openpype/hosts/nuke/api/pipeline.py @@ -14,7 +14,7 @@ IPublishHost ) from openpype.settings import get_current_project_settings -from openpype.lib import register_event_callback, Logger +from openpype.lib import register_event_callback, emit_event, Logger from openpype.pipeline import ( register_loader_plugin_path, register_creator_plugin_path, @@ -32,7 +32,6 @@ ROOT_DATA_KNOB, INSTANCE_DATA_KNOB, get_main_window, - add_publish_knob, WorkfileSettings, # TODO: remove this once workfile builder will be removed process_workfile_builder, @@ -178,6 +177,10 @@ def add_nuke_callbacks(): # set apply all workfile settings on script load and save nuke.addOnScriptLoad(WorkfileSettings().set_context_settings) + # Emit events + nuke.addOnCreate(_on_scene_open, nodeClass="Root") + nuke.addOnScriptSave(_on_scene_save) + if nuke_settings["nuke-dirmap"]["enabled"]: log.info("Added Nuke's dir-mapping callback ...") @@ -636,3 +639,14 @@ def select_instance(instance): """ instance_node = instance.transient_data["node"] instance_node["selected"].setValue(True) + + +def _on_scene_open(*args): + emit_event("open") + + +def _on_scene_save(*args): + skip = os.getenv("OP_NUKE_SKIP_SAVE_EVENT") + if skip: + return + emit_event("after.save") diff --git a/openpype/modules/ftrack/ftrack_module.py b/openpype/modules/ftrack/ftrack_module.py index 2042367a7e0..44031b530db 100644 --- a/openpype/modules/ftrack/ftrack_module.py +++ b/openpype/modules/ftrack/ftrack_module.py @@ -3,6 +3,7 @@ import collections import platform +from openpype.lib import register_event_callback from openpype.modules import ( click_wrap, OpenPypeModule, @@ -10,9 +11,15 @@ IPluginPaths, ISettingsChangeListener ) -from openpype.settings import SaveWarningExc +from openpype.settings import SaveWarningExc, get_project_settings from openpype.lib import Logger +from openpype.pipeline import ( + get_current_project_name, + get_current_asset_name, + get_current_task_name +) + FTRACK_MODULE_DIR = os.path.dirname(os.path.abspath(__file__)) _URL_NOT_SET = object() @@ -62,6 +69,133 @@ def initialize(self, settings): self.timers_manager_connector = None self._timers_manager_module = None + # Hooks when a file has been opened or saved + register_event_callback("open", self.after_file_open) + register_event_callback("after.save", self.after_file_save) + + def find_ftrack_task_entity( + self, session, project_name, asset_name, task_name + ): + project_entity = session.query( + "Project where full_name is \"{}\"".format(project_name) + ).first() + if not project_entity: + self.log.warning( + "Couldn't find project \"{}\" in Ftrack.".format(project_name) + ) + return + + potential_task_entities = session.query(( + "TypedContext where parent.name is \"{}\" and project_id is \"{}\"" + ).format(asset_name, project_entity["id"])).all() + filtered_entities = [] + for _entity in potential_task_entities: + if ( + _entity.entity_type.lower() == "task" + and _entity["name"] == task_name + ): + filtered_entities.append(_entity) + + if not filtered_entities: + self.log.warning(( + "Couldn't find task \"{}\" under parent \"{}\" in Ftrack." + ).format(task_name, asset_name)) + return + + if len(filtered_entities) > 1: + self.log.warning(( + "Found more than one task \"{}\"" + " under parent \"{}\" in Ftrack." + ).format(task_name, asset_name)) + return + + return filtered_entities[0] + + def after_file_open(self, event): + self._auto_update_task_status("status_change_on_file_open") + + def after_file_save(self, event): + self._auto_update_task_status("status_change_on_file_save") + + def _auto_update_task_status(self, setting_mapping_section): + # Create ftrack session + try: + session = self.create_ftrack_session() + except Exception: # noqa + self.log.warning("Couldn't create ftrack session.", exc_info=True) + return + # ---------------------- + + project_name = get_current_project_name() + project_settings = get_project_settings(project_name) + + # Do we want/need/can update the status? + change_task_status = project_settings["ftrack"]["application_handlers"]["change_task_status"] + if not change_task_status["enabled"]: + return + + mapping = change_task_status[setting_mapping_section] + if not mapping: + # No rules registered, skip. + return + # -------------------------------------- + + asset_name = get_current_asset_name() + task_name = get_current_task_name() + + # Find the entity + entity = self.find_ftrack_task_entity(session, project_name, asset_name, task_name) + if not entity: + # No valid entity found, quit. + return + # --------------- + + ent_path = "/".join([ent["name"] for ent in entity["link"]]) + actual_status = entity["status"]["name"] + + # Find the next status + if "__ignore__" in mapping: + ignored_statuses = [status for status in mapping["__ignore__"]] + if actual_status in ignored_statuses: + # We can exit, the status is flagged to be ignored. + return + + # Removing to avoid looping on it + mapping.pop("__ignore__") + + next_status = None + + for to_status, from_statuses in mapping.items(): + from_statuses = [status for status in from_statuses] + if "__any__" in from_statuses: + next_status = to_status + # Not breaking, in case a better mapping is set after. + continue + + if actual_status in from_statuses: + next_status = to_status + # We found a valid mapping (other that __any__) we stop looking. + break + + if not next_status: + # No valid next status found, skip. + return + # -------------------- + + # Change the status on ftrack + try: + query = "Status where name is \"{}\"".format(next_status) + next_status_obj = session.query(query).one() + + entity["status"] = next_status_obj + session.commit() + self.log.debug("Changing current task status to \"{}\" <{}>".format(next_status, ent_path)) + except Exception: # noqa + session.rollback() + msg = "Status \"{}\" in presets wasn't found on Ftrack entity type \"{}\"".format(next_status, + entity.entity_type) + self.log.error(msg) + def get_ftrack_url(self): """Resolved ftrack url. diff --git a/openpype/modules/ftrack/launch_hooks/post_ftrack_changes.py b/openpype/modules/ftrack/launch_hooks/post_ftrack_changes.py deleted file mode 100644 index 1876ff20ebc..00000000000 --- a/openpype/modules/ftrack/launch_hooks/post_ftrack_changes.py +++ /dev/null @@ -1,165 +0,0 @@ -import os - -import ftrack_api -from openpype.settings import get_project_settings -from openpype.lib.applications import PostLaunchHook, LaunchTypes - - -class PostFtrackHook(PostLaunchHook): - order = None - launch_types = {LaunchTypes.local} - - def execute(self): - project_name = self.data.get("project_name") - asset_name = self.data.get("asset_name") - task_name = self.data.get("task_name") - - missing_context_keys = set() - if not project_name: - missing_context_keys.add("project_name") - if not asset_name: - missing_context_keys.add("asset_name") - if not task_name: - missing_context_keys.add("task_name") - - if missing_context_keys: - missing_keys_str = ", ".join([ - "\"{}\"".format(key) for key in missing_context_keys - ]) - self.log.debug("Hook {} skipped. Missing data keys: {}".format( - self.__class__.__name__, missing_keys_str - )) - return - - required_keys = ("FTRACK_SERVER", "FTRACK_API_USER", "FTRACK_API_KEY") - for key in required_keys: - if not os.environ.get(key): - self.log.debug(( - "Missing required environment \"{}\"" - " for Ftrack after launch procedure." - ).format(key)) - return - - try: - session = ftrack_api.Session(auto_connect_event_hub=True) - self.log.debug("Ftrack session created") - except Exception: - self.log.warning("Couldn't create Ftrack session") - return - - try: - entity = self.find_ftrack_task_entity( - session, project_name, asset_name, task_name - ) - if entity: - self.ftrack_status_change(session, entity, project_name) - - except Exception: - self.log.warning( - "Couldn't finish Ftrack procedure.", exc_info=True - ) - return - - finally: - session.close() - - def find_ftrack_task_entity( - self, session, project_name, asset_name, task_name - ): - project_entity = session.query( - "Project where full_name is \"{}\"".format(project_name) - ).first() - if not project_entity: - self.log.warning( - "Couldn't find project \"{}\" in Ftrack.".format(project_name) - ) - return - - potential_task_entities = session.query(( - "TypedContext where parent.name is \"{}\" and project_id is \"{}\"" - ).format(asset_name, project_entity["id"])).all() - filtered_entities = [] - for _entity in potential_task_entities: - if ( - _entity.entity_type.lower() == "task" - and _entity["name"] == task_name - ): - filtered_entities.append(_entity) - - if not filtered_entities: - self.log.warning(( - "Couldn't find task \"{}\" under parent \"{}\" in Ftrack." - ).format(task_name, asset_name)) - return - - if len(filtered_entities) > 1: - self.log.warning(( - "Found more than one task \"{}\"" - " under parent \"{}\" in Ftrack." - ).format(task_name, asset_name)) - return - - return filtered_entities[0] - - def ftrack_status_change(self, session, entity, project_name): - project_settings = get_project_settings(project_name) - status_update = project_settings["ftrack"]["events"]["status_update"] - if not status_update["enabled"]: - self.log.debug( - "Status changes are disabled for project \"{}\"".format( - project_name - ) - ) - return - - status_mapping = status_update["mapping"] - if not status_mapping: - self.log.warning( - "Project \"{}\" does not have set status changes.".format( - project_name - ) - ) - return - - actual_status = entity["status"]["name"].lower() - already_tested = set() - ent_path = "/".join( - [ent["name"] for ent in entity["link"]] - ) - while True: - next_status_name = None - for key, value in status_mapping.items(): - if key in already_tested: - continue - - value = [i.lower() for i in value] - if actual_status in value or "__any__" in value: - if key != "__ignore__": - next_status_name = key - already_tested.add(key) - break - already_tested.add(key) - - if next_status_name is None: - break - - try: - query = "Status where name is \"{}\"".format( - next_status_name - ) - status = session.query(query).one() - - entity["status"] = status - session.commit() - self.log.debug("Changing status to \"{}\" <{}>".format( - next_status_name, ent_path - )) - break - - except Exception: - session.rollback() - msg = ( - "Status \"{}\" in presets wasn't found" - " on Ftrack entity type \"{}\"" - ).format(next_status_name, entity.entity_type) - self.log.warning(msg) diff --git a/openpype/modules/ftrack/lib/ftrack_base_handler.py b/openpype/modules/ftrack/lib/ftrack_base_handler.py index 55400c22aba..d499f07ae44 100644 --- a/openpype/modules/ftrack/lib/ftrack_base_handler.py +++ b/openpype/modules/ftrack/lib/ftrack_base_handler.py @@ -13,7 +13,7 @@ from openpype_modules.ftrack import ftrack_server -class MissingPermision(Exception): +class MissingPermission(Exception): def __init__(self, message=None): if message is None: message = 'Ftrack' @@ -101,7 +101,7 @@ def wrapper_register(*args, **kwargs): self.log.info(( '{} "{}" - Registered successfully ({:.4f}sec)' ).format(self.type, label, run_time)) - except MissingPermision as MPE: + except MissingPermission as MPE: self.log.info(( '!{} "{}" - You\'re missing required {} permissions' ).format(self.type, label, str(MPE))) diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index e2ca334b5f3..f7eb9668b8c 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -47,22 +47,6 @@ "user_assignment": { "enabled": true }, - "status_update": { - "enabled": true, - "mapping": { - "In Progress": [ - "__any__" - ], - "Ready": [ - "Not Ready" - ], - "__ignore__": [ - "in progress", - "omitted", - "on hold" - ] - } - }, "status_task_to_parent": { "enabled": true, "parent_object_types": [ @@ -130,18 +114,6 @@ } }, "user_handlers": { - "application_launch_statuses": { - "enabled": true, - "ignored_statuses": [ - "In Progress", - "Omitted", - "On hold", - "Approved" - ], - "status_change": { - "In Progress": [] - } - }, "create_update_attributes": { "role_list": [ "Pypeclub", @@ -223,6 +195,31 @@ ] } }, + "application_handlers": { + "change_task_status": { + "enabled": true, + "status_change_on_file_open": { + "In Progress": [ + "__any__" + ], + "__ignore__": [ + "In Progress", + "Omitted", + "On Hold" + ] + }, + "status_change_on_file_save": { + "In Progress": [ + "__any__" + ], + "__ignore__": [ + "In Progress", + "Omitted", + "On Hold" + ] + } + } + }, "publish": { "CollectFtrackFamily": { "enabled": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json index d6efb118b9f..6ef8b560575 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json @@ -145,28 +145,6 @@ } ] }, - { - "type": "dict", - "key": "status_update", - "label": "Update status on task action", - "is_group": true, - "checkbox_key": "enabled", - "children": [ - { - "type": "boolean", - "key": "enabled", - "label": "Enabled" - }, - { - "key": "mapping", - "type": "dict-modifiable", - "object_type": { - "type": "list", - "object_type": "text" - } - } - ] - }, { "type": "dict", "key": "status_task_to_parent", @@ -447,41 +425,6 @@ "key": "user_handlers", "label": "User Actions/Events", "children": [ - { - "type": "dict", - "key": "application_launch_statuses", - "is_group": true, - "label": "Application - Status change on launch", - "checkbox_key": "enabled", - "children": [ - { - "type": "boolean", - "key": "enabled", - "label": "Enabled" - }, - { - "type": "label", - "label": "Do not change status if current status is:" - }, - { - "type": "list", - "key": "ignored_statuses", - "object_type": "text" - }, - { - "type": "label", - "label": "Change task's status to left side if current task status is in list on right side." - }, - { - "type": "dict-modifiable", - "key": "status_change", - "object_type": { - "type": "list", - "object_type": "text" - } - } - ] - }, { "type": "dict", "key": "create_update_attributes", @@ -708,6 +651,53 @@ } ] }, + { + "type": "dict", + "key": "application_handlers", + "label": "Application Events", + "children": [ + { + "type": "dict", + "key": "change_task_status", + "is_group": true, + "label": "Status change", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "label", + "label": "On Open - (Change task's status to left side if current task status is in the list on right side)" + }, + { + "type": "dict-modifiable", + "key": "status_change_on_file_open", + "label": "Status change on file open", + "object_type": { + "type": "list", + "object_type": "ftrack-task-statuses" + } + }, + { + "type": "label", + "label": "On Save - (Change task's status to left side if current task status is in the list on right side)" + }, + { + "type": "dict-modifiable", + "key": "status_change_on_file_save", + "label": "Status change on file save", + "object_type": { + "type": "list", + "object_type": "ftrack-task-statuses" + } + } + ] + } + ] + }, { "type": "dict", "collapsible": true,