mirror of
https://github.com/ynput/ayon-core.git
synced 2026-01-01 08:24:53 +01:00
Merge pull request #821 from pypeclub/feature/version_to_task_with_right_settings
Version to task with right settings
This commit is contained in:
commit
c2a3d20667
10 changed files with 289 additions and 73 deletions
|
|
@ -1,25 +1,40 @@
|
||||||
from pype.modules.ftrack import BaseEvent
|
from pype.modules.ftrack import BaseEvent
|
||||||
from pype.api import get_project_settings
|
|
||||||
|
|
||||||
|
|
||||||
class VersionToTaskStatus(BaseEvent):
|
class VersionToTaskStatus(BaseEvent):
|
||||||
|
"""Propagates status from version to task when changed."""
|
||||||
def launch(self, session, event):
|
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 project_id, entities_info in filtered_entities_info.items():
|
||||||
for entity in event['data'].get('entities', []):
|
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
|
# Filter AssetVersions
|
||||||
if entity["entityType"] != "assetversion":
|
if entity_info["entityType"] != "assetversion":
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Skip if statusid not in keys (in changes)
|
# 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:
|
if not keys or "statusid" not in keys:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Get new version task name
|
# Get new version task name
|
||||||
version_status_id = (
|
version_status_id = (
|
||||||
entity
|
entity_info
|
||||||
.get("changes", {})
|
.get("changes", {})
|
||||||
.get("statusid", {})
|
.get("statusid", {})
|
||||||
.get("new", {})
|
.get("new", {})
|
||||||
|
|
@ -29,74 +44,162 @@ class VersionToTaskStatus(BaseEvent):
|
||||||
if not version_status_id:
|
if not version_status_id:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
# Get project id from entity info
|
||||||
version_status = session.get("Status", version_status_id)
|
project_id = entity_info["parents"][-1]["entityId"]
|
||||||
except Exception:
|
if project_id not in filtered_entity_info:
|
||||||
self.log.warning(
|
filtered_entity_info[project_id] = []
|
||||||
"Troubles with query status id [ {} ]".format(
|
filtered_entity_info[project_id].append(entity_info)
|
||||||
version_status_id
|
return filtered_entity_info
|
||||||
),
|
|
||||||
exc_info=True
|
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
|
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
|
# Qeury statuses
|
||||||
version = session.get("AssetVersion", entity["entityId"])
|
statusese_by_obj_id = self.statuses_for_tasks(
|
||||||
task = version.get("task")
|
session, task_entities, project_entity
|
||||||
if not task:
|
)
|
||||||
continue
|
# Prepare status names by their ids
|
||||||
|
status_name_by_id = {
|
||||||
project_entity = self.get_project_from_entity(task)
|
status_entity["id"]: status_entity["name"]
|
||||||
project_name = project_entity["full_name"]
|
for status_entity in version_status_entities
|
||||||
project_settings = get_project_settings(project_name)
|
}
|
||||||
|
for entity_info in entities_info:
|
||||||
# Load status mapping from presets
|
entity_id = entity_info["entityId"]
|
||||||
status_mapping = (
|
status_id = entity_info["changes"]["statusid"]["new"]
|
||||||
project_settings["ftrack"]["events"]["status_version_to_task"])
|
status_name = status_name_by_id.get(status_id)
|
||||||
# Skip if mapping is empty
|
if not status_name:
|
||||||
if not status_mapping:
|
|
||||||
continue
|
continue
|
||||||
|
status_name_low = status_name.lower()
|
||||||
|
|
||||||
# Lower version status name and check if has mapping
|
# Lower version status name and check if has mapping
|
||||||
version_status = version_status_orig.lower()
|
|
||||||
new_status_names = []
|
new_status_names = []
|
||||||
mapped = status_mapping.get(version_status)
|
mapped = status_mapping.get(status_name_low)
|
||||||
if mapped:
|
if mapped:
|
||||||
new_status_names.extend(list(mapped))
|
new_status_names.extend(list(mapped))
|
||||||
|
|
||||||
new_status_names.append(version_status)
|
new_status_names.append(status_name_low)
|
||||||
|
|
||||||
self.log.debug(
|
self.log.debug(
|
||||||
"Processing AssetVersion status change: [ {} ]".format(
|
"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
|
# Lower all names from presets
|
||||||
new_status_names = [name.lower() for name in new_status_names]
|
new_status_names = [name.lower() for name in new_status_names]
|
||||||
|
task_statuses_by_low_name = statusese_by_obj_id[type_id]
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
new_status = None
|
new_status = None
|
||||||
for status_name in new_status_names:
|
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
|
continue
|
||||||
|
|
||||||
# store object of found status
|
# 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(
|
self.log.debug("Status to set: [ {} ]".format(
|
||||||
new_status["name"]
|
new_status["name"]
|
||||||
))
|
))
|
||||||
|
|
@ -110,16 +213,15 @@ class VersionToTaskStatus(BaseEvent):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Get full path to task for logging
|
# 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
|
# Setting task status
|
||||||
try:
|
try:
|
||||||
task["status"] = new_status
|
task_entity["status"] = new_status
|
||||||
session.commit()
|
session.commit()
|
||||||
self.log.debug("[ {} ] Status updated to [ {} ]".format(
|
self.log.debug("[ {} ] Status updated to [ {} ]".format(
|
||||||
ent_path, new_status['name']
|
ent_path, new_status["name"]
|
||||||
))
|
))
|
||||||
except Exception:
|
except Exception:
|
||||||
session.rollback()
|
session.rollback()
|
||||||
|
|
@ -128,6 +230,22 @@ class VersionToTaskStatus(BaseEvent):
|
||||||
exc_info=True
|
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):
|
def register(session, plugins_presets):
|
||||||
'''Register plugin. Called when used as an plugin.'''
|
'''Register plugin. Called when used as an plugin.'''
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ from pype.modules.ftrack.ftrack_server.lib import (
|
||||||
get_ftrack_event_mongo_info
|
get_ftrack_event_mongo_info
|
||||||
)
|
)
|
||||||
|
|
||||||
import socket_thread
|
from pype.modules.ftrack.ftrack_server import socket_thread
|
||||||
|
|
||||||
|
|
||||||
class MongoPermissionsError(Exception):
|
class MongoPermissionsError(Exception):
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,10 @@ import inspect
|
||||||
|
|
||||||
import ftrack_api
|
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
|
# Required - Needed for connection to Ftrack
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import functools
|
import functools
|
||||||
import time
|
import time
|
||||||
from pype.api import Logger
|
from pype.api import Logger
|
||||||
|
from pype.settings import get_project_settings
|
||||||
|
|
||||||
import ftrack_api
|
import ftrack_api
|
||||||
from pype.modules.ftrack import ftrack_server
|
from pype.modules.ftrack import ftrack_server
|
||||||
|
|
||||||
|
|
@ -581,3 +583,67 @@ class BaseHandler(object):
|
||||||
return self.session.query(
|
return self.session.query(
|
||||||
"Project where id is {}".format(project_data["id"])
|
"Project where id is {}".format(project_data["id"])
|
||||||
).one()
|
).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
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,18 @@ class PypeCommands:
|
||||||
return return_code
|
return return_code
|
||||||
|
|
||||||
def launch_eventservercli(self, args):
|
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):
|
def publish(self, gui, paths):
|
||||||
pass
|
pass
|
||||||
|
|
|
||||||
|
|
@ -71,11 +71,13 @@
|
||||||
"status_version_to_task": {
|
"status_version_to_task": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"mapping": {
|
"mapping": {
|
||||||
"Complete": [
|
"Approved": [
|
||||||
"Approved",
|
|
||||||
"Complete"
|
"Complete"
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
"asset_types_to_skip": [
|
||||||
|
"scene"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"first_version_status": {
|
"first_version_status": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
|
|
|
||||||
|
|
@ -179,4 +179,4 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -136,7 +136,8 @@
|
||||||
"enabled": false
|
"enabled": false
|
||||||
},
|
},
|
||||||
"ValidateAttributes": {
|
"ValidateAttributes": {
|
||||||
"enabled": false
|
"enabled": false,
|
||||||
|
"attributes": {}
|
||||||
},
|
},
|
||||||
"ExtractCameraAlembic": {
|
"ExtractCameraAlembic": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
|
|
|
||||||
|
|
@ -87,4 +87,4 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"filters": {}
|
"filters": {}
|
||||||
}
|
}
|
||||||
|
|
@ -204,20 +204,38 @@
|
||||||
"label": "Sync status from Version to Task",
|
"label": "Sync status from Version to Task",
|
||||||
"checkbox_key": "enabled",
|
"checkbox_key": "enabled",
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"key": "enabled",
|
"key": "enabled",
|
||||||
"label": "Enabled"
|
"label": "Enabled"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "dict-modifiable",
|
"type": "label",
|
||||||
"key": "mapping",
|
"label": "<b>Change Task status based on a changed Version status.</b><br/>Version's new status on the <b>left</b> will trigger a change of a task status to the first available from the list on <b>right</b>.<br/> - if no status from the list is available it will use the same status as the version."
|
||||||
"object_type":
|
},
|
||||||
|
{
|
||||||
|
"type": "dict-modifiable",
|
||||||
|
"key": "mapping",
|
||||||
|
"object_type":
|
||||||
|
{
|
||||||
|
"type": "list",
|
||||||
|
"object_type": "text"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "separator"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "label",
|
||||||
|
"label": "<b>Disable<b/> event if status was changed on specific Asset type."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "list",
|
"type": "list",
|
||||||
|
"label": "Asset types (short)",
|
||||||
|
"key": "asset_types_to_skip",
|
||||||
"object_type": "text"
|
"object_type": "text"
|
||||||
}
|
}
|
||||||
}]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "dict",
|
"type": "dict",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue