diff --git a/pype/modules/ftrack/events/action_push_frame_values_to_task.py b/pype/modules/ftrack/events/action_push_frame_values_to_task.py index 87d9d5afe9..6df27682e0 100644 --- a/pype/modules/ftrack/events/action_push_frame_values_to_task.py +++ b/pype/modules/ftrack/events/action_push_frame_values_to_task.py @@ -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 diff --git a/pype/modules/ftrack/events/event_push_frame_values_to_task.py b/pype/modules/ftrack/events/event_push_frame_values_to_task.py index 061002c13f..8e277679bd 100644 --- a/pype/modules/ftrack/events/event_push_frame_values_to_task.py +++ b/pype/modules/ftrack/events/event_push_frame_values_to_task.py @@ -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 = {} diff --git a/pype/modules/ftrack/lib/ftrack_action_handler.py b/pype/modules/ftrack/lib/ftrack_action_handler.py index f42469c675..d95c81955e 100644 --- a/pype/modules/ftrack/lib/ftrack_action_handler.py +++ b/pype/modules/ftrack/lib/ftrack_action_handler.py @@ -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): diff --git a/pype/settings/defaults/project_settings/ftrack.json b/pype/settings/defaults/project_settings/ftrack.json index 92c51d0a3b..debc92f2b5 100644 --- a/pype/settings/defaults/project_settings/ftrack.json +++ b/pype/settings/defaults/project_settings/ftrack.json @@ -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": { 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 c128d4b751..cbff26e135 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 @@ -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" } ] },