Merge pull request #821 from pypeclub/feature/version_to_task_with_right_settings

Version to task with right settings
This commit is contained in:
Milan Kolar 2020-12-16 20:42:16 +01:00 committed by GitHub
commit c2a3d20667
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 289 additions and 73 deletions

View file

@ -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.'''

View file

@ -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):

View file

@ -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

View file

@ -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

View file

@ -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

View file

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

View file

@ -179,4 +179,4 @@
}
}
}
}
}

View file

@ -136,7 +136,8 @@
"enabled": false
},
"ValidateAttributes": {
"enabled": false
"enabled": false,
"attributes": {}
},
"ExtractCameraAlembic": {
"enabled": true,

View file

@ -87,4 +87,4 @@
]
},
"filters": {}
}
}

View file

@ -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": "<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."
},
{
"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",
"label": "Asset types (short)",
"key": "asset_types_to_skip",
"object_type": "text"
}
}]
]
},
{
"type": "dict",