diff --git a/pype/modules/ftrack/events/event_version_to_task_statuses.py b/pype/modules/ftrack/events/event_version_to_task_statuses.py index 0ea72be1cb..ed47d2f8a9 100644 --- a/pype/modules/ftrack/events/event_version_to_task_statuses.py +++ b/pype/modules/ftrack/events/event_version_to_task_statuses.py @@ -1,25 +1,40 @@ from pype.modules.ftrack import BaseEvent -from pype.api import get_project_settings class VersionToTaskStatus(BaseEvent): + """Propagates status from version to task when changed.""" def launch(self, session, event): - '''Propagates status from version to task when changed''' + # Filter event entities + # - output is dictionary where key is project id and event info in + # value + filtered_entities_info = self.filter_entity_info(event) + if not filtered_entities_info: + return - # start of event procedure ---------------------------------- - for entity in event['data'].get('entities', []): + for project_id, entities_info in filtered_entities_info.items(): + self.process_by_project(session, event, project_id, entities_info) + + # TODO remove `join_query_keys` as it should be in `BaseHandler` + @staticmethod + def join_query_keys(keys): + """Helper to join keys to query.""" + return ",".join(["\"{}\"".format(key) for key in keys]) + + def filter_entity_info(self, event): + filtered_entity_info = {} + for entity_info in event["data"].get("entities", []): # Filter AssetVersions - if entity["entityType"] != "assetversion": + if entity_info["entityType"] != "assetversion": continue # Skip if statusid not in keys (in changes) - keys = entity.get("keys") + keys = entity_info.get("keys") if not keys or "statusid" not in keys: continue # Get new version task name version_status_id = ( - entity + entity_info .get("changes", {}) .get("statusid", {}) .get("new", {}) @@ -29,74 +44,162 @@ class VersionToTaskStatus(BaseEvent): if not version_status_id: continue - try: - version_status = session.get("Status", version_status_id) - except Exception: - self.log.warning( - "Troubles with query status id [ {} ]".format( - version_status_id - ), - exc_info=True + # Get project id from entity info + project_id = entity_info["parents"][-1]["entityId"] + if project_id not in filtered_entity_info: + filtered_entity_info[project_id] = [] + filtered_entity_info[project_id].append(entity_info) + return filtered_entity_info + + def process_by_project(self, session, event, project_id, entities_info): + # Check for project data if event is enabled for event handler + status_mapping = None + project_entity = self.get_project_entity_from_event( + session, event, project_id + ) + project_settings = self.get_settings_for_project( + session, event, project_entity=project_entity + ) + + project_name = project_entity["full_name"] + # Load status mapping from presets + event_settings = ( + project_settings["ftrack"]["events"]["status_version_to_task"] + ) + # Skip if event is not enabled or status mapping is not set + if not event_settings["enabled"]: + self.log.debug("Project \"{}\" has disabled {}".format( + project_name, self.__class__.__name__ + )) + return + + _status_mapping = event_settings["mapping"] + if not _status_mapping: + self.log.debug( + "Project \"{}\" does not have set mapping for {}".format( + project_name, self.__class__.__name__ ) + ) + return - if not version_status: + status_mapping = { + key.lower(): value + for key, value in _status_mapping.items() + } + + asset_types_to_skip = [ + short_name.lower() + for short_name in event_settings["asset_types_to_skip"] + ] + + # Collect entity ids + asset_version_ids = set() + for entity_info in entities_info: + asset_version_ids.add(entity_info["entityId"]) + + # Query tasks for AssetVersions + _asset_version_entities = session.query( + "AssetVersion where task_id != none and id in ({})".format( + self.join_query_keys(asset_version_ids) + ) + ).all() + if not _asset_version_entities: + return + + # Filter asset versions by asset type and store their task_ids + task_ids = set() + asset_version_entities = [] + for asset_version in _asset_version_entities: + if asset_types_to_skip: + short_name = asset_version["asset"]["type"]["short"].lower() + if short_name in asset_types_to_skip: + continue + asset_version_entities.append(asset_version) + task_ids.add(asset_version["task_id"]) + + # Skipt if `task_ids` are empty + if not task_ids: + return + + task_entities = session.query( + "select link from Task where id in ({})".format( + self.join_query_keys(task_ids) + ) + ).all() + task_entities_by_id = { + task_entiy["id"]: task_entiy + for task_entiy in task_entities + } + + # Prepare asset version by their id + asset_versions_by_id = { + asset_version["id"]: asset_version + for asset_version in asset_version_entities + } + + # Query status entities + status_ids = set() + for entity_info in entities_info: + # Skip statuses of asset versions without task + if entity_info["entityId"] not in asset_versions_by_id: continue + status_ids.add(entity_info["changes"]["statusid"]["new"]) - version_status_orig = version_status["name"] + version_status_entities = session.query( + "select id, name from Status where id in ({})".format( + self.join_query_keys(status_ids) + ) + ).all() - # Get entities necessary for processing - version = session.get("AssetVersion", entity["entityId"]) - task = version.get("task") - if not task: - continue - - project_entity = self.get_project_from_entity(task) - project_name = project_entity["full_name"] - project_settings = get_project_settings(project_name) - - # Load status mapping from presets - status_mapping = ( - project_settings["ftrack"]["events"]["status_version_to_task"]) - # Skip if mapping is empty - if not status_mapping: + # Qeury statuses + statusese_by_obj_id = self.statuses_for_tasks( + session, task_entities, project_entity + ) + # Prepare status names by their ids + status_name_by_id = { + status_entity["id"]: status_entity["name"] + for status_entity in version_status_entities + } + for entity_info in entities_info: + entity_id = entity_info["entityId"] + status_id = entity_info["changes"]["statusid"]["new"] + status_name = status_name_by_id.get(status_id) + if not status_name: continue + status_name_low = status_name.lower() # Lower version status name and check if has mapping - version_status = version_status_orig.lower() new_status_names = [] - mapped = status_mapping.get(version_status) + mapped = status_mapping.get(status_name_low) if mapped: new_status_names.extend(list(mapped)) - new_status_names.append(version_status) + new_status_names.append(status_name_low) self.log.debug( "Processing AssetVersion status change: [ {} ]".format( - version_status_orig + status_name ) ) + asset_version = asset_versions_by_id[entity_id] + task_entity = task_entities_by_id[asset_version["task_id"]] + type_id = task_entity["type_id"] + # Lower all names from presets new_status_names = [name.lower() for name in new_status_names] - - if version["asset"]["type"]["short"].lower() == "scene": - continue - - project_schema = project_entity["project_schema"] - # Get all available statuses for Task - statuses = project_schema.get_statuses("Task", task["type_id"]) - # map lowered status name with it's object - stat_names_low = { - status["name"].lower(): status for status in statuses - } + task_statuses_by_low_name = statusese_by_obj_id[type_id] new_status = None for status_name in new_status_names: - if status_name not in stat_names_low: + if status_name not in task_statuses_by_low_name: + self.log.debug(( + "Task does not have status name \"{}\" available." + ).format(status_name)) continue # store object of found status - new_status = stat_names_low[status_name] + new_status = task_statuses_by_low_name[status_name] self.log.debug("Status to set: [ {} ]".format( new_status["name"] )) @@ -110,16 +213,15 @@ class VersionToTaskStatus(BaseEvent): ) ) continue - # Get full path to task for logging - ent_path = "/".join([ent["name"] for ent in task["link"]]) + ent_path = "/".join([ent["name"] for ent in task_entity["link"]]) # Setting task status try: - task["status"] = new_status + task_entity["status"] = new_status session.commit() self.log.debug("[ {} ] Status updated to [ {} ]".format( - ent_path, new_status['name'] + ent_path, new_status["name"] )) except Exception: session.rollback() @@ -128,6 +230,22 @@ class VersionToTaskStatus(BaseEvent): exc_info=True ) + def statuses_for_tasks(self, session, task_entities, project_entity): + task_type_ids = set() + for task_entity in task_entities: + task_type_ids.add(task_entity["type_id"]) + + project_schema = project_entity["project_schema"] + output = {} + for task_type_id in task_type_ids: + statuses = project_schema.get_statuses("Task", task_type_id) + output[task_type_id] = { + status["name"].lower(): status + for status in statuses + } + + return output + def register(session, plugins_presets): '''Register plugin. Called when used as an plugin.''' diff --git a/pype/modules/ftrack/ftrack_server/event_server_cli.py b/pype/modules/ftrack/ftrack_server/event_server_cli.py index dbf2a2dc10..96581f0a38 100644 --- a/pype/modules/ftrack/ftrack_server/event_server_cli.py +++ b/pype/modules/ftrack/ftrack_server/event_server_cli.py @@ -23,7 +23,7 @@ from pype.modules.ftrack.ftrack_server.lib import ( get_ftrack_event_mongo_info ) -import socket_thread +from pype.modules.ftrack.ftrack_server import socket_thread class MongoPermissionsError(Exception): diff --git a/pype/modules/ftrack/ftrack_server/ftrack_server.py b/pype/modules/ftrack/ftrack_server/ftrack_server.py index af48bfadc8..93c7cd3a67 100644 --- a/pype/modules/ftrack/ftrack_server/ftrack_server.py +++ b/pype/modules/ftrack/ftrack_server/ftrack_server.py @@ -8,10 +8,10 @@ import inspect import ftrack_api -from pype.api import Logger +from pype.lib import PypeLogger -log = Logger().get_logger(__name__) +log = PypeLogger().get_logger(__name__) """ # Required - Needed for connection to Ftrack diff --git a/pype/modules/ftrack/lib/ftrack_base_handler.py b/pype/modules/ftrack/lib/ftrack_base_handler.py index e928f2fb88..669381af90 100644 --- a/pype/modules/ftrack/lib/ftrack_base_handler.py +++ b/pype/modules/ftrack/lib/ftrack_base_handler.py @@ -1,6 +1,8 @@ import functools import time from pype.api import Logger +from pype.settings import get_project_settings + import ftrack_api from pype.modules.ftrack import ftrack_server @@ -581,3 +583,67 @@ class BaseHandler(object): return self.session.query( "Project where id is {}".format(project_data["id"]) ).one() + + def get_project_entity_from_event(self, session, event, project_id): + """Load or query and fill project entity from/to event data. + + Project data are stored by ftrack id because in most cases it is + easier to access project id than project name. + + Args: + session (ftrack_api.Session): Current session. + event (ftrack_api.Event): Processed event by session. + project_id (str): Ftrack project id. + """ + if not project_id: + raise ValueError( + "Entered `project_id` is not valid. {} ({})".format( + str(project_id), str(type(project_id)) + ) + ) + # Try to get project entity from event + project_entities = event["data"].get("project_entities") + if not project_entities: + project_entities = {} + event["data"]["project_entities"] = project_entities + + project_entity = project_entities.get(project_id) + if not project_entity: + # Get project entity from task and store to event + project_entity = session.get("Project", project_id) + event["data"]["project_entities"][project_id] = project_entity + return project_entity + + def get_settings_for_project( + self, session, event, project_id=None, project_entity=None + ): + """Load or fill pype's project settings from event data. + + Project data are stored by ftrack id because in most cases it is + easier to access project id than project name. + + Args: + session (ftrack_api.Session): Current session. + event (ftrack_api.Event): Processed event by session. + project_id (str): Ftrack project id. Must be entered if + project_entity is not. + project_entity (ftrack_api.Entity): Project entity. Must be entered + if project_id is not. + """ + if not project_entity: + project_entity = self.get_project_entity_from_event( + session, event, project_id + ) + + project_name = project_entity["full_name"] + + project_settings_by_id = event["data"].get("project_settings") + if not project_settings_by_id: + project_settings_by_id = {} + event["data"]["project_settings"] = project_settings_by_id + + project_settings = project_settings_by_id.get(project_id) + if not project_settings: + project_settings = get_project_settings(project_name) + event["data"]["project_settings"][project_id] = project_settings + return project_settings diff --git a/pype/pype_commands.py b/pype/pype_commands.py index cc54deb2ee..f504728ca1 100644 --- a/pype/pype_commands.py +++ b/pype/pype_commands.py @@ -60,7 +60,18 @@ class PypeCommands: return return_code def launch_eventservercli(self, args): - pass + from pype.modules import ftrack + from pype.lib import execute + + fname = os.path.join( + os.path.dirname(os.path.abspath(ftrack.__file__)), + "ftrack_server", + "event_server_cli.py" + ) + + return execute([ + sys.executable, "-u", fname + ]) def publish(self, gui, paths): pass diff --git a/pype/settings/defaults/project_settings/ftrack.json b/pype/settings/defaults/project_settings/ftrack.json index 5481574ef8..8a597c3e6a 100644 --- a/pype/settings/defaults/project_settings/ftrack.json +++ b/pype/settings/defaults/project_settings/ftrack.json @@ -71,11 +71,13 @@ "status_version_to_task": { "enabled": true, "mapping": { - "Complete": [ - "Approved", + "Approved": [ "Complete" ] - } + }, + "asset_types_to_skip": [ + "scene" + ] }, "first_version_status": { "enabled": true, diff --git a/pype/settings/defaults/project_settings/global.json b/pype/settings/defaults/project_settings/global.json index da56fd34e7..4e2b9fce81 100644 --- a/pype/settings/defaults/project_settings/global.json +++ b/pype/settings/defaults/project_settings/global.json @@ -179,4 +179,4 @@ } } } -} +} \ No newline at end of file diff --git a/pype/settings/defaults/project_settings/maya.json b/pype/settings/defaults/project_settings/maya.json index c779d495c4..b8c0dffa26 100644 --- a/pype/settings/defaults/project_settings/maya.json +++ b/pype/settings/defaults/project_settings/maya.json @@ -136,7 +136,8 @@ "enabled": false }, "ValidateAttributes": { - "enabled": false + "enabled": false, + "attributes": {} }, "ExtractCameraAlembic": { "enabled": true, diff --git a/pype/settings/defaults/project_settings/nuke.json b/pype/settings/defaults/project_settings/nuke.json index 61001914d2..82dcf23694 100644 --- a/pype/settings/defaults/project_settings/nuke.json +++ b/pype/settings/defaults/project_settings/nuke.json @@ -87,4 +87,4 @@ ] }, "filters": {} -} +} \ No newline at end of file diff --git a/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_ftrack.json b/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_ftrack.json index ea01400e94..df04e6a8aa 100644 --- a/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_ftrack.json +++ b/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_ftrack.json @@ -204,20 +204,38 @@ "label": "Sync status from Version to Task", "checkbox_key": "enabled", "children": [ - { - "type": "boolean", - "key": "enabled", - "label": "Enabled" - }, - { - "type": "dict-modifiable", - "key": "mapping", - "object_type": + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "label", + "label": "Change Task status based on a changed Version status.
Version's new status on the left will trigger a change of a task status to the first available from the list on right.
- if no status from the list is available it will use the same status as the version." + }, + { + "type": "dict-modifiable", + "key": "mapping", + "object_type": + { + "type": "list", + "object_type": "text" + } + }, + { + "type": "separator" + }, + { + "type": "label", + "label": "Disable event if status was changed on specific Asset type." + }, { "type": "list", + "label": "Asset types (short)", + "key": "asset_types_to_skip", "object_type": "text" } - }] + ] }, { "type": "dict",