mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge pull request #860 from pypeclub/feature/sync_hier_attributes_with_settings
Sync hier attributes with settings
This commit is contained in:
commit
3ea01fadab
5 changed files with 246 additions and 145 deletions
|
|
@ -59,18 +59,22 @@ class PushHierValuesToNonHier(ServerAction):
|
|||
)
|
||||
|
||||
# configurable
|
||||
interest_entity_types = ["Shot"]
|
||||
interest_attributes = ["frameStart", "frameEnd"]
|
||||
role_list = ["Pypeclub", "Administrator", "Project Manager"]
|
||||
settings_key = "sync_hier_entity_attributes"
|
||||
settings_enabled_key = "action_enabled"
|
||||
|
||||
def discover(self, session, entities, event):
|
||||
""" Validation """
|
||||
# Check if selection is valid
|
||||
is_valid = False
|
||||
for ent in event["data"]["selection"]:
|
||||
# Ignore entities that are not tasks or projects
|
||||
if ent["entityType"].lower() in ("task", "show"):
|
||||
return True
|
||||
return False
|
||||
is_valid = True
|
||||
break
|
||||
|
||||
if is_valid:
|
||||
is_valid = self.valid_roles(session, entities, event)
|
||||
return is_valid
|
||||
|
||||
def launch(self, session, entities, event):
|
||||
self.log.debug("{}: Creating job".format(self.label))
|
||||
|
|
@ -88,7 +92,7 @@ class PushHierValuesToNonHier(ServerAction):
|
|||
session.commit()
|
||||
|
||||
try:
|
||||
result = self.propagate_values(session, entities)
|
||||
result = self.propagate_values(session, event, entities)
|
||||
job["status"] = "done"
|
||||
session.commit()
|
||||
|
||||
|
|
@ -111,9 +115,9 @@ class PushHierValuesToNonHier(ServerAction):
|
|||
job["status"] = "failed"
|
||||
session.commit()
|
||||
|
||||
def attrs_configurations(self, session, object_ids):
|
||||
def attrs_configurations(self, session, object_ids, interest_attributes):
|
||||
attrs = session.query(self.cust_attrs_query.format(
|
||||
self.join_query_keys(self.interest_attributes),
|
||||
self.join_query_keys(interest_attributes),
|
||||
self.join_query_keys(object_ids)
|
||||
)).all()
|
||||
|
||||
|
|
@ -129,7 +133,14 @@ class PushHierValuesToNonHier(ServerAction):
|
|||
output[obj_id].append(attr)
|
||||
return output, hiearchical
|
||||
|
||||
def propagate_values(self, session, selected_entities):
|
||||
def propagate_values(self, session, event, selected_entities):
|
||||
ftrack_settings = self.get_ftrack_settings(
|
||||
session, event, selected_entities
|
||||
)
|
||||
action_settings = (
|
||||
ftrack_settings[self.settings_frack_subkey][self.settings_key]
|
||||
)
|
||||
|
||||
project_entity = self.get_project_from_entity(selected_entities[0])
|
||||
selected_ids = [entity["id"] for entity in selected_entities]
|
||||
|
||||
|
|
@ -138,7 +149,7 @@ class PushHierValuesToNonHier(ServerAction):
|
|||
))
|
||||
interest_entity_types = tuple(
|
||||
ent_type.lower()
|
||||
for ent_type in self.interest_entity_types
|
||||
for ent_type in action_settings["interest_entity_types"]
|
||||
)
|
||||
all_object_types = session.query("ObjectType").all()
|
||||
object_types_by_low_name = {
|
||||
|
|
@ -158,9 +169,10 @@ class PushHierValuesToNonHier(ServerAction):
|
|||
for obj_type in destination_object_types
|
||||
)
|
||||
|
||||
interest_attributes = action_settings["interest_attributes"]
|
||||
# Find custom attributes definitions
|
||||
attrs_by_obj_id, hier_attrs = self.attrs_configurations(
|
||||
session, destination_object_type_ids
|
||||
session, destination_object_type_ids, interest_attributes
|
||||
)
|
||||
# Filter destination object types if they have any object specific
|
||||
# custom attribute
|
||||
|
|
|
|||
|
|
@ -7,8 +7,6 @@ from pype.modules.ftrack import BaseEvent
|
|||
|
||||
class PushFrameValuesToTaskEvent(BaseEvent):
|
||||
# Ignore event handler by default
|
||||
ignore_me = True
|
||||
|
||||
cust_attrs_query = (
|
||||
"select id, key, object_type_id, is_hierarchical, default"
|
||||
" from CustomAttributeConfiguration"
|
||||
|
|
@ -27,36 +25,7 @@ class PushFrameValuesToTaskEvent(BaseEvent):
|
|||
_cached_changes = []
|
||||
_max_delta = 30
|
||||
|
||||
# Configrable (lists)
|
||||
interest_entity_types = {"Shot"}
|
||||
interest_attributes = {"frameStart", "frameEnd"}
|
||||
|
||||
@staticmethod
|
||||
def join_keys(keys):
|
||||
return ",".join(["\"{}\"".format(key) for key in keys])
|
||||
|
||||
@classmethod
|
||||
def task_object_id(cls, session):
|
||||
if cls._cached_task_object_id is None:
|
||||
task_object_type = session.query(
|
||||
"ObjectType where name is \"Task\""
|
||||
).one()
|
||||
cls._cached_task_object_id = task_object_type["id"]
|
||||
return cls._cached_task_object_id
|
||||
|
||||
@classmethod
|
||||
def interest_object_ids(cls, session):
|
||||
if cls._cached_interest_object_ids is None:
|
||||
object_types = session.query(
|
||||
"ObjectType where name in ({})".format(
|
||||
cls.join_keys(cls.interest_entity_types)
|
||||
)
|
||||
).all()
|
||||
cls._cached_interest_object_ids = tuple(
|
||||
object_type["id"]
|
||||
for object_type in object_types
|
||||
)
|
||||
return cls._cached_interest_object_ids
|
||||
settings_key = "sync_hier_entity_attributes"
|
||||
|
||||
def session_user_id(self, session):
|
||||
if self._cached_user_id is None:
|
||||
|
|
@ -67,30 +36,146 @@ class PushFrameValuesToTaskEvent(BaseEvent):
|
|||
return self._cached_user_id
|
||||
|
||||
def launch(self, session, event):
|
||||
interesting_data, changed_keys_by_object_id = (
|
||||
self.extract_interesting_data(session, event)
|
||||
filtered_entities_info = self.filter_entities_info(event)
|
||||
if not filtered_entities_info:
|
||||
return
|
||||
|
||||
for project_id, entities_info in filtered_entities_info.items():
|
||||
self.process_by_project(session, event, project_id, entities_info)
|
||||
|
||||
def filter_entities_info(self, event):
|
||||
# Filter if event contain relevant data
|
||||
entities_info = event["data"].get("entities")
|
||||
if not entities_info:
|
||||
return
|
||||
|
||||
entities_info_by_project_id = {}
|
||||
for entity_info in entities_info:
|
||||
# Care only about tasks
|
||||
if entity_info.get("entityType") != "task":
|
||||
continue
|
||||
|
||||
# Skip `Task` entity type
|
||||
if entity_info["entity_type"].lower() == "task":
|
||||
continue
|
||||
|
||||
# Care only about changes of status
|
||||
changes = entity_info.get("changes")
|
||||
if not changes:
|
||||
continue
|
||||
|
||||
# Get project id from entity info
|
||||
project_id = None
|
||||
for parent_item in reversed(entity_info["parents"]):
|
||||
if parent_item["entityType"] == "show":
|
||||
project_id = parent_item["entityId"]
|
||||
break
|
||||
|
||||
if project_id is None:
|
||||
continue
|
||||
|
||||
if project_id not in entities_info_by_project_id:
|
||||
entities_info_by_project_id[project_id] = []
|
||||
entities_info_by_project_id[project_id].append(entity_info)
|
||||
|
||||
return entities_info_by_project_id
|
||||
|
||||
def process_by_project(self, session, event, project_id, entities_info):
|
||||
project_name = self.get_project_name_from_event(
|
||||
session, event, project_id
|
||||
)
|
||||
# Load settings
|
||||
project_settings = self.get_project_settings_from_event(
|
||||
event, project_name
|
||||
)
|
||||
# Load status mapping from presets
|
||||
event_settings = (
|
||||
project_settings
|
||||
["ftrack"]
|
||||
["events"]
|
||||
["sync_hier_entity_attributes"]
|
||||
)
|
||||
# Skip if event is not enabled
|
||||
if not event_settings["enabled"]:
|
||||
self.log.debug("Project \"{}\" has disabled {}".format(
|
||||
project_name, self.__class__.__name__
|
||||
))
|
||||
return
|
||||
|
||||
interest_attributes = event_settings["interest_attributes"]
|
||||
if not interest_attributes:
|
||||
self.log.info((
|
||||
"Project \"{}\" does not have filled 'interest_attributes',"
|
||||
" skipping."
|
||||
))
|
||||
return
|
||||
interest_entity_types = event_settings["interest_entity_types"]
|
||||
if not interest_entity_types:
|
||||
self.log.info((
|
||||
"Project \"{}\" does not have filled 'interest_entity_types',"
|
||||
" skipping."
|
||||
))
|
||||
return
|
||||
|
||||
# Filter entities info with changes
|
||||
interesting_data, changed_keys_by_object_id = self.filter_changes(
|
||||
session, event, entities_info, interest_attributes
|
||||
)
|
||||
if not interesting_data:
|
||||
return
|
||||
|
||||
entities = self.get_entities(session, interesting_data)
|
||||
# Prepare object types
|
||||
object_types = session.query("select id, name from ObjectType").all()
|
||||
object_types_by_name = {}
|
||||
for object_type in object_types:
|
||||
name_low = object_type["name"].lower()
|
||||
object_types_by_name[name_low] = object_type
|
||||
|
||||
# Prepare task object id
|
||||
task_object_id = object_types_by_name["task"]["id"]
|
||||
|
||||
# Collect object type ids based on settings
|
||||
interest_object_ids = []
|
||||
for entity_type in interest_entity_types:
|
||||
_entity_type = entity_type.lower()
|
||||
object_type = object_types_by_name.get(_entity_type)
|
||||
if not object_type:
|
||||
self.log.warning("Couldn't find object type \"{}\"".format(
|
||||
entity_type
|
||||
))
|
||||
|
||||
interest_object_ids.append(object_type["id"])
|
||||
|
||||
# Query entities by filtered data and object ids
|
||||
entities = self.get_entities(
|
||||
session, interesting_data, interest_object_ids
|
||||
)
|
||||
if not entities:
|
||||
return
|
||||
|
||||
entities_by_id = {
|
||||
entity["id"]: entity
|
||||
# Pop not found entities from interesting data
|
||||
entity_ids = set(
|
||||
entity["id"]
|
||||
for entity in entities
|
||||
}
|
||||
)
|
||||
for entity_id in tuple(interesting_data.keys()):
|
||||
if entity_id not in entities_by_id:
|
||||
if entity_id not in entity_ids:
|
||||
interesting_data.pop(entity_id)
|
||||
|
||||
attrs_by_obj_id, hier_attrs = self.attrs_configurations(session)
|
||||
# Add task object type to list
|
||||
attr_obj_ids = list(interest_object_ids)
|
||||
attr_obj_ids.append(task_object_id)
|
||||
|
||||
attrs_by_obj_id, hier_attrs = self.attrs_configurations(
|
||||
session, attr_obj_ids, interest_attributes
|
||||
)
|
||||
|
||||
task_object_id = self.task_object_id(session)
|
||||
task_attrs = attrs_by_obj_id.get(task_object_id)
|
||||
|
||||
changed_keys = set()
|
||||
# Skip keys that are not both in hierachical and type specific
|
||||
for object_id, keys in changed_keys_by_object_id.items():
|
||||
changed_keys |= set(keys)
|
||||
object_id_attrs = attrs_by_obj_id.get(object_id)
|
||||
for key in keys:
|
||||
if key not in hier_attrs:
|
||||
|
|
@ -113,8 +198,8 @@ class PushFrameValuesToTaskEvent(BaseEvent):
|
|||
"There is not created Custom Attributes {} "
|
||||
" for entity types: {}"
|
||||
).format(
|
||||
self.join_keys(self.interest_attributes),
|
||||
self.join_keys(self.interest_entity_types)
|
||||
self.join_query_keys(interest_attributes),
|
||||
self.join_query_keys(interest_entity_types)
|
||||
))
|
||||
return
|
||||
|
||||
|
|
@ -124,16 +209,24 @@ class PushFrameValuesToTaskEvent(BaseEvent):
|
|||
if task_attrs:
|
||||
task_entities = self.get_task_entities(session, interesting_data)
|
||||
|
||||
task_entities_by_id = {}
|
||||
task_entity_ids = set()
|
||||
parent_id_by_task_id = {}
|
||||
for task_entity in task_entities:
|
||||
task_entities_by_id[task_entity["id"]] = task_entity
|
||||
parent_id_by_task_id[task_entity["id"]] = task_entity["parent_id"]
|
||||
task_id = task_entity["id"]
|
||||
task_entity_ids.add(task_id)
|
||||
parent_id_by_task_id[task_id] = task_entity["parent_id"]
|
||||
|
||||
changed_keys = set()
|
||||
for keys in changed_keys_by_object_id.values():
|
||||
changed_keys |= set(keys)
|
||||
self.finalize(
|
||||
session, interesting_data,
|
||||
changed_keys, attrs_by_obj_id, hier_attrs,
|
||||
task_entity_ids, parent_id_by_task_id
|
||||
)
|
||||
|
||||
def finalize(
|
||||
self, session, interesting_data,
|
||||
changed_keys, attrs_by_obj_id, hier_attrs,
|
||||
task_entity_ids, parent_id_by_task_id
|
||||
):
|
||||
attr_id_to_key = {}
|
||||
for attr_confs in attrs_by_obj_id.values():
|
||||
for key in changed_keys:
|
||||
|
|
@ -147,12 +240,12 @@ class PushFrameValuesToTaskEvent(BaseEvent):
|
|||
attr_id_to_key[custom_attr_id] = key
|
||||
|
||||
entity_ids = (
|
||||
set(interesting_data.keys()) | set(task_entities_by_id.keys())
|
||||
set(interesting_data.keys()) | task_entity_ids
|
||||
)
|
||||
attr_ids = set(attr_id_to_key.keys())
|
||||
|
||||
current_values_by_id = self.current_values(
|
||||
session, attr_ids, entity_ids, task_entities_by_id, hier_attrs
|
||||
session, attr_ids, entity_ids, task_entity_ids, hier_attrs
|
||||
)
|
||||
|
||||
for entity_id, current_values in current_values_by_id.items():
|
||||
|
|
@ -214,45 +307,9 @@ class PushFrameValuesToTaskEvent(BaseEvent):
|
|||
session.rollback()
|
||||
self.log.warning("Changing of values failed.", exc_info=True)
|
||||
|
||||
def current_values(
|
||||
self, session, attr_ids, entity_ids, task_entities_by_id, hier_attrs
|
||||
def filter_changes(
|
||||
self, session, event, entities_info, interest_attributes
|
||||
):
|
||||
current_values_by_id = {}
|
||||
if not attr_ids or not entity_ids:
|
||||
return current_values_by_id
|
||||
joined_conf_ids = self.join_keys(attr_ids)
|
||||
joined_entity_ids = self.join_keys(entity_ids)
|
||||
|
||||
call_expr = [{
|
||||
"action": "query",
|
||||
"expression": self.cust_attr_query.format(
|
||||
joined_entity_ids, joined_conf_ids
|
||||
)
|
||||
}]
|
||||
if hasattr(session, "call"):
|
||||
[values] = session.call(call_expr)
|
||||
else:
|
||||
[values] = session._call(call_expr)
|
||||
|
||||
for item in values["data"]:
|
||||
entity_id = item["entity_id"]
|
||||
attr_id = item["configuration_id"]
|
||||
if entity_id in task_entities_by_id and attr_id in hier_attrs:
|
||||
continue
|
||||
|
||||
if entity_id not in current_values_by_id:
|
||||
current_values_by_id[entity_id] = {}
|
||||
current_values_by_id[entity_id][attr_id] = item["value"]
|
||||
return current_values_by_id
|
||||
|
||||
def extract_interesting_data(self, session, event):
|
||||
# Filter if event contain relevant data
|
||||
entities_info = event["data"].get("entities")
|
||||
if not entities_info:
|
||||
return
|
||||
|
||||
# for key, value in event["data"].items():
|
||||
# self.log.info("{}: {}".format(key, value))
|
||||
session_user_id = self.session_user_id(session)
|
||||
user_data = event["data"].get("user")
|
||||
changed_by_session = False
|
||||
|
|
@ -264,18 +321,10 @@ class PushFrameValuesToTaskEvent(BaseEvent):
|
|||
interesting_data = {}
|
||||
changed_keys_by_object_id = {}
|
||||
for entity_info in entities_info:
|
||||
# Care only about tasks
|
||||
if entity_info.get("entityType") != "task":
|
||||
continue
|
||||
|
||||
# Care only about changes of status
|
||||
changes = entity_info.get("changes") or {}
|
||||
if not changes:
|
||||
continue
|
||||
|
||||
# Care only about changes if specific keys
|
||||
entity_changes = {}
|
||||
for key in self.interest_attributes:
|
||||
changes = entity_info["changes"]
|
||||
for key in interest_attributes:
|
||||
if key in changes:
|
||||
entity_changes[key] = changes[key]["new"]
|
||||
|
||||
|
|
@ -307,48 +356,66 @@ class PushFrameValuesToTaskEvent(BaseEvent):
|
|||
if not entity_changes:
|
||||
continue
|
||||
|
||||
# Do not care about "Task" entity_type
|
||||
task_object_id = self.task_object_id(session)
|
||||
object_id = entity_info.get("objectTypeId")
|
||||
if not object_id or object_id == task_object_id:
|
||||
continue
|
||||
|
||||
entity_id = entity_info["entityId"]
|
||||
object_id = entity_info["objectTypeId"]
|
||||
interesting_data[entity_id] = entity_changes
|
||||
if object_id not in changed_keys_by_object_id:
|
||||
changed_keys_by_object_id[object_id] = set()
|
||||
|
||||
changed_keys_by_object_id[object_id] |= set(entity_changes.keys())
|
||||
|
||||
return interesting_data, changed_keys_by_object_id
|
||||
|
||||
def get_entities(self, session, interesting_data):
|
||||
entities = session.query(
|
||||
"TypedContext where id in ({})".format(
|
||||
self.join_keys(interesting_data.keys())
|
||||
)
|
||||
).all()
|
||||
def current_values(
|
||||
self, session, attr_ids, entity_ids, task_entity_ids, hier_attrs
|
||||
):
|
||||
current_values_by_id = {}
|
||||
if not attr_ids or not entity_ids:
|
||||
return current_values_by_id
|
||||
joined_conf_ids = self.join_query_keys(attr_ids)
|
||||
joined_entity_ids = self.join_query_keys(entity_ids)
|
||||
|
||||
output = []
|
||||
interest_object_ids = self.interest_object_ids(session)
|
||||
for entity in entities:
|
||||
if entity["object_type_id"] in interest_object_ids:
|
||||
output.append(entity)
|
||||
return output
|
||||
call_expr = [{
|
||||
"action": "query",
|
||||
"expression": self.cust_attr_query.format(
|
||||
joined_entity_ids, joined_conf_ids
|
||||
)
|
||||
}]
|
||||
if hasattr(session, "call"):
|
||||
[values] = session.call(call_expr)
|
||||
else:
|
||||
[values] = session._call(call_expr)
|
||||
|
||||
for item in values["data"]:
|
||||
entity_id = item["entity_id"]
|
||||
attr_id = item["configuration_id"]
|
||||
if entity_id in task_entity_ids and attr_id in hier_attrs:
|
||||
continue
|
||||
|
||||
if entity_id not in current_values_by_id:
|
||||
current_values_by_id[entity_id] = {}
|
||||
current_values_by_id[entity_id][attr_id] = item["value"]
|
||||
return current_values_by_id
|
||||
|
||||
def get_entities(self, session, interesting_data, interest_object_ids):
|
||||
return session.query((
|
||||
"select id from TypedContext"
|
||||
" where id in ({}) and object_type_id in ({})"
|
||||
).format(
|
||||
self.join_query_keys(interesting_data.keys()),
|
||||
self.join_query_keys(interest_object_ids)
|
||||
)).all()
|
||||
|
||||
def get_task_entities(self, session, interesting_data):
|
||||
return session.query(
|
||||
"Task where parent_id in ({})".format(
|
||||
self.join_keys(interesting_data.keys())
|
||||
"select id, parent_id from Task where parent_id in ({})".format(
|
||||
self.join_query_keys(interesting_data.keys())
|
||||
)
|
||||
).all()
|
||||
|
||||
def attrs_configurations(self, session):
|
||||
object_ids = list(self.interest_object_ids(session))
|
||||
object_ids.append(self.task_object_id(session))
|
||||
|
||||
def attrs_configurations(self, session, object_ids, interest_attributes):
|
||||
attrs = session.query(self.cust_attrs_query.format(
|
||||
self.join_keys(self.interest_attributes),
|
||||
self.join_keys(object_ids)
|
||||
self.join_query_keys(interest_attributes),
|
||||
self.join_query_keys(object_ids)
|
||||
)).all()
|
||||
|
||||
output = {}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ class BaseAction(BaseHandler):
|
|||
type = 'Action'
|
||||
|
||||
settings_frack_subkey = "user_handlers"
|
||||
settings_enabled_key = "enabled"
|
||||
|
||||
def __init__(self, session):
|
||||
'''Expects a ftrack_api.Session instance'''
|
||||
|
|
@ -298,8 +299,9 @@ class BaseAction(BaseHandler):
|
|||
settings = (
|
||||
ftrack_settings[self.settings_frack_subkey][self.settings_key]
|
||||
)
|
||||
if not settings.get("enabled", True):
|
||||
return False
|
||||
if self.settings_enabled_key:
|
||||
if not settings.get(self.settings_enabled_key, True):
|
||||
return False
|
||||
|
||||
user_role_list = self.get_user_roles_from_event(session, event)
|
||||
if not self.roles_check(settings.get("role_list"), user_role_list):
|
||||
|
|
|
|||
|
|
@ -9,15 +9,21 @@
|
|||
"not ready"
|
||||
]
|
||||
},
|
||||
"push_frame_values_to_task": {
|
||||
"sync_hier_entity_attributes": {
|
||||
"enabled": true,
|
||||
"interest_entity_types": [
|
||||
"shot",
|
||||
"asset build"
|
||||
"Shot",
|
||||
"Asset Build"
|
||||
],
|
||||
"interest_attributess": [
|
||||
"interest_attributes": [
|
||||
"frameStart",
|
||||
"frameEnd"
|
||||
],
|
||||
"action_enabled": true,
|
||||
"role_list": [
|
||||
"Pypeclub",
|
||||
"Administrator",
|
||||
"Project Manager"
|
||||
]
|
||||
},
|
||||
"thumbnail_updates": {
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@
|
|||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"key": "push_frame_values_to_task",
|
||||
"key": "sync_hier_entity_attributes",
|
||||
"label": "Sync Hierarchical and Entity Attributes",
|
||||
"checkbox_key": "enabled",
|
||||
"children": [
|
||||
|
|
@ -81,12 +81,26 @@
|
|||
},
|
||||
{
|
||||
"type": "list",
|
||||
"key": "interest_attributess",
|
||||
"key": "interest_attributes",
|
||||
"label": "Attributes to sync",
|
||||
"object_type": {
|
||||
"type": "text",
|
||||
"multiline": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "action_enabled",
|
||||
"label": "Enable Action"
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"key": "role_list",
|
||||
"label": "Roles for action",
|
||||
"object_type": "text"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue