Merge pull request #860 from pypeclub/feature/sync_hier_attributes_with_settings

Sync hier attributes with settings
This commit is contained in:
Milan Kolar 2021-01-08 23:16:54 +01:00 committed by GitHub
commit 3ea01fadab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 246 additions and 145 deletions

View file

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

View file

@ -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 = {}

View file

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

View file

@ -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": {

View file

@ -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"
}
]
},