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",