From 6dab474ba89632e96ba85808476ab38aaa394ce2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 21 Dec 2020 12:51:58 +0100 Subject: [PATCH 01/13] next task update use partially settings --- .../ftrack/events/event_next_task_update.py | 302 +++++++++--------- 1 file changed, 146 insertions(+), 156 deletions(-) diff --git a/pype/modules/ftrack/events/event_next_task_update.py b/pype/modules/ftrack/events/event_next_task_update.py index deb789f981..ac50a7e0aa 100644 --- a/pype/modules/ftrack/events/event_next_task_update.py +++ b/pype/modules/ftrack/events/event_next_task_update.py @@ -1,19 +1,28 @@ -import operator import collections from pype.modules.ftrack import BaseEvent class NextTaskUpdate(BaseEvent): - def filter_entities_info(self, session, event): + def launch(self, session, event): + '''Propagates status from version to task when changed''' + + 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 - first_filtered_entities = [] + filtered_entities_info = {} for entity_info in entities_info: - # Care only about tasks - if entity_info.get("entityType") != "task": + # Care only about Task `entity_type` + if entity_info.get("entity_type") != "Task": continue # Care only about changes of status @@ -25,190 +34,159 @@ class NextTaskUpdate(BaseEvent): ): continue - first_filtered_entities.append(entity_info) + project_id = None + for parent_info in reversed(entity_info["parents"]): + if parent_info["entityType"] == "show": + project_id = parent_info["entityId"] + break - if not first_filtered_entities: - return first_filtered_entities + if project_id: + filtered_entities_info[project_id].append(entity_info) + return filtered_entities_info - status_ids = [ - entity_info["changes"]["statusid"]["new"] - for entity_info in first_filtered_entities - ] - statuses_by_id = self.get_statuses_by_id( - session, status_ids=status_ids + def process_by_project(self, session, event, project_id, _entities_info): + 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 ) - # Make sure `entity_type` is "Task" - task_object_type = session.query( - "select id, name from ObjectType where name is \"Task\"" - ).one() - # Care only about tasks having status with state `Done` - filtered_entities = [] - for entity_info in first_filtered_entities: - if entity_info["objectTypeId"] != task_object_type["id"]: - continue - status_id = entity_info["changes"]["statusid"]["new"] - status_entity = statuses_by_id[status_id] - if status_entity["state"]["name"].lower() == "done": - filtered_entities.append(entity_info) + project_name = project_entity["full_name"] - return filtered_entities + # Load status mapping from presets + event_settings = ( + project_settings["ftrack"]["events"]["next_task_update"] + ) + if not event_settings["enabled"]: + self.log.debug("Project \"{}\" has disabled {}.".format( + project_name, self.__class__.__name__ + )) + return - def get_parents_by_id(self, session, entities_info): - parent_ids = [ - "\"{}\"".format(entity_info["parentId"]) - for entity_info in entities_info - ] - parent_entities = session.query( - "TypedContext where id in ({})".format(", ".join(parent_ids)) - ).all() + statuses = session.query("Status").all() - return { - entity["id"]: entity - for entity in parent_entities - } - - def get_tasks_by_id(self, session, parent_ids): - joined_parent_ids = ",".join([ - "\"{}\"".format(parent_id) - for parent_id in parent_ids - ]) - task_entities = session.query( - "Task where parent_id in ({})".format(joined_parent_ids) - ).all() - - return { - entity["id"]: entity - for entity in task_entities - } - - def get_statuses_by_id(self, session, task_entities=None, status_ids=None): - if task_entities is None and status_ids is None: - return {} - - if status_ids is None: - status_ids = [] - for task_entity in task_entities: - status_ids.append(task_entity["status_id"]) - - if not status_ids: - return {} - - status_entities = session.query( - "Status where id in ({})".format(", ".join(status_ids)) - ).all() - - return { - entity["id"]: entity - for entity in status_entities - } - - def get_sorted_task_types(self, session): - data = { - _type: _type.get("sort") - for _type in session.query("Type").all() - if _type.get("sort") is not None - } - - return [ - item[0] - for item in sorted(data.items(), key=operator.itemgetter(1)) - ] - - def launch(self, session, event): - '''Propagates status from version to task when changed''' - - entities_info = self.filter_entities_info(session, event) + entities_info = self.filter_by_status_state(_entities_info, statuses) if not entities_info: return - parents_by_id = self.get_parents_by_id(session, entities_info) - tasks_by_id = self.get_tasks_by_id( - session, tuple(parents_by_id.keys()) + parent_ids = set() + event_task_ids_by_parent_id = collections.defaultdict(list) + for entity_info in entities_info: + parent_id = entity_info["parentId"] + entity_id = entity_info["entityId"] + parent_ids.add(parent_id) + event_task_ids_by_parent_id[parent_id].append(entity_id) + + # From now it doesn't matter what was in event data + task_entities = session.query( + ( + "select id, type_id, status_id, parent_id, link from Task" + " where parent_id in ({})" + ).format(self.join_query_keys(parent_ids)) + ).all() + + tasks_by_parent_id = collections.defaultdict(list) + for task_entity in task_entities: + tasks_by_parent_id[task_entity["parent_id"]].append(task_entity) + + self.set_next_task_statuses( + session, + tasks_by_parent_id, + event_task_ids_by_parent_id, + statuses ) - tasks_to_parent_id = collections.defaultdict(list) - for task_entity in tasks_by_id.values(): - tasks_to_parent_id[task_entity["parent_id"]].append(task_entity) + def filter_by_status_state(self, entities_info, statuses): + statuses_by_id = { + status["id"]: status + for status in statuses + } - statuses_by_id = self.get_statuses_by_id(session, tasks_by_id.values()) + # Care only about tasks having status with state `Done` + filtered_entities_info = [] + for entity_info in entities_info: + status_id = entity_info["changes"]["statusid"]["new"] + status_entity = statuses_by_id[status_id] + if status_entity["state"]["name"].lower() == "done": + filtered_entities_info.append(entity_info) + return filtered_entities_info + def set_next_task_statuses( + self, + session, + tasks_by_parent_id, + event_task_ids_by_parent_id, + statuses + ): + statuses_by_low_name = { + status["name"].lower(): status + for status in statuses + } next_status_name = "Ready" - next_status = session.query( - "Status where name is \"{}\"".format(next_status_name) - ).first() + next_status = statuses_by_low_name.get(next_status_name.lower()) if not next_status: self.log.warning("Couldn't find status with name \"{}\"".format( next_status_name )) return - for entity_info in entities_info: - parent_id = entity_info["parentId"] - task_id = entity_info["entityId"] - task_entity = tasks_by_id[task_id] + statuses_by_id = { + status["id"].lower(): status + for status in statuses + } - all_same_type_taks_done = True - for parents_task in tasks_to_parent_id[parent_id]: - if ( - parents_task["id"] == task_id - or parents_task["type_id"] != task_entity["type_id"] - ): + sorted_task_type_ids = self.get_sorted_task_type_ids(session) + + for parent_id, _task_entities in tasks_by_parent_id.items(): + task_entities_by_type_id = collections.defaultdict(list) + for _task_entity in _task_entities: + type_id = _task_entity["type_id"] + task_entities_by_type_id[type_id].append(_task_entity) + + event_ids = set(event_task_ids_by_parent_id[parent_id]) + next_tasks = [] + for type_id in sorted_task_type_ids: + if type_id not in task_entities_by_type_id: continue - parents_task_status = statuses_by_id[parents_task["status_id"]] - low_status_name = parents_task_status["name"].lower() - # Skip if task's status name "Omitted" - if low_status_name == "omitted": - continue - - low_state_name = parents_task_status["state"]["name"].lower() - if low_state_name != "done": - all_same_type_taks_done = False + all_in_type_done = True + task_entities = task_entities_by_type_id[type_id] + if not event_ids: + next_tasks = task_entities break - if not all_same_type_taks_done: + for task_entity in task_entities: + task_id = task_entity["id"] + if task_id in event_ids: + event_ids.remove(task_id) + + task_status = statuses_by_id[task_entity["status_id"]] + low_status_name = task_status["name"].lower() + if low_status_name == "omitted": + continue + + low_state_name = task_status["state"]["name"].lower() + if low_state_name != "done": + all_in_type_done = False + break + + if not all_in_type_done: + break + + if not next_tasks: continue - # Prepare all task types - sorted_task_types = self.get_sorted_task_types(session) - sorted_task_types_len = len(sorted_task_types) - - from_idx = None - for idx, task_type in enumerate(sorted_task_types): - if task_type["id"] == task_entity["type_id"]: - from_idx = idx + 1 - break - - # Current task type is last in order - if from_idx is None or from_idx >= sorted_task_types_len: - continue - - next_task_type_id = None - next_task_type_tasks = [] - for idx in range(from_idx, sorted_task_types_len): - next_task_type = sorted_task_types[idx] - for parents_task in tasks_to_parent_id[parent_id]: - if next_task_type_id is None: - if parents_task["type_id"] != next_task_type["id"]: - continue - next_task_type_id = next_task_type["id"] - - if parents_task["type_id"] == next_task_type_id: - next_task_type_tasks.append(parents_task) - - if next_task_type_id is not None: - break - - for next_task_entity in next_task_type_tasks: - if next_task_entity["status"]["name"].lower() != "not ready": + for task_entity in next_tasks: + task_status = statuses_by_id[task_entity["status_id"]] + if task_status["name"].lower() != "not ready": continue ent_path = "/".join( - [ent["name"] for ent in next_task_entity["link"]] + [ent["name"] for ent in task_entity["link"]] ) try: - next_task_entity["status"] = next_status + task_entity["status_id"] = next_status["id"] session.commit() self.log.info( "\"{}\" updated status to \"{}\"".format( @@ -224,6 +202,18 @@ class NextTaskUpdate(BaseEvent): exc_info=True ) + def get_sorted_task_type_ids(self, session): + types_by_order = collections.defaultdict(list) + for _type in session.query("Type").all(): + sort_oder = _type.get("sort") + if sort_oder is not None: + types_by_order[sort_oder].append(_type["id"]) + + types = [] + for sort_oder in sorted(types_by_order.keys()): + types.extend(types_by_order[sort_oder]) + return types + def register(session, plugins_presets): NextTaskUpdate(session, plugins_presets).register() From 7fea99738105aeda22997d3566b092693b9dd9a8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 21 Dec 2020 12:55:32 +0100 Subject: [PATCH 02/13] fix variable type --- pype/modules/ftrack/events/event_next_task_update.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/modules/ftrack/events/event_next_task_update.py b/pype/modules/ftrack/events/event_next_task_update.py index ac50a7e0aa..7eb6368964 100644 --- a/pype/modules/ftrack/events/event_next_task_update.py +++ b/pype/modules/ftrack/events/event_next_task_update.py @@ -19,7 +19,7 @@ class NextTaskUpdate(BaseEvent): if not entities_info: return - filtered_entities_info = {} + filtered_entities_info = collections.defaultdict(list) for entity_info in entities_info: # Care only about Task `entity_type` if entity_info.get("entity_type") != "Task": From deb8e6e7ca9d5879f60a7be2bd2020b650716df0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 21 Dec 2020 13:59:28 +0100 Subject: [PATCH 03/13] use settings for status changes --- .../ftrack/events/event_next_task_update.py | 77 ++++++++++++++----- 1 file changed, 57 insertions(+), 20 deletions(-) diff --git a/pype/modules/ftrack/events/event_next_task_update.py b/pype/modules/ftrack/events/event_next_task_update.py index 7eb6368964..6252985e1b 100644 --- a/pype/modules/ftrack/events/event_next_task_update.py +++ b/pype/modules/ftrack/events/event_next_task_update.py @@ -94,7 +94,9 @@ class NextTaskUpdate(BaseEvent): session, tasks_by_parent_id, event_task_ids_by_parent_id, - statuses + statuses, + project_entity, + event_settings ) def filter_by_status_state(self, entities_info, statuses): @@ -117,25 +119,33 @@ class NextTaskUpdate(BaseEvent): session, tasks_by_parent_id, event_task_ids_by_parent_id, - statuses + statuses, + project_entity, + event_settings ): - statuses_by_low_name = { - status["name"].lower(): status - for status in statuses - } - next_status_name = "Ready" - next_status = statuses_by_low_name.get(next_status_name.lower()) - if not next_status: - self.log.warning("Couldn't find status with name \"{}\"".format( - next_status_name - )) - return - statuses_by_id = { - status["id"].lower(): status + status["id"]: status for status in statuses } + ignored_statuses = set( + status_name.lower() + for status_name in event_settings["ignored_statuses"] + ) + mapping = { + status_from.lower(): status_to.lower() + for status_from, status_to in event_settings["mapping"].items() + } + + task_type_ids = set() + for task_entities in tasks_by_parent_id.values(): + for task_entity in task_entities: + task_type_ids.add(task_entity["type_id"]) + + statusese_by_obj_id = self.statuses_for_tasks( + task_type_ids, project_entity + ) + sorted_task_type_ids = self.get_sorted_task_type_ids(session) for parent_id, _task_entities in tasks_by_parent_id.items(): @@ -163,7 +173,7 @@ class NextTaskUpdate(BaseEvent): task_status = statuses_by_id[task_entity["status_id"]] low_status_name = task_status["name"].lower() - if low_status_name == "omitted": + if low_status_name in ignored_statuses: continue low_state_name = task_status["state"]["name"].lower() @@ -179,29 +189,56 @@ class NextTaskUpdate(BaseEvent): for task_entity in next_tasks: task_status = statuses_by_id[task_entity["status_id"]] - if task_status["name"].lower() != "not ready": + old_status_name = task_status["name"].lower() + new_task_name = mapping.get(old_status_name) + if not new_task_name: + self.log.debug( + "Didn't found mapping for status \"{}\".".format( + task_status["name"] + ) + ) continue ent_path = "/".join( [ent["name"] for ent in task_entity["link"]] ) + type_id = task_entity["type_id"] + new_status = statusese_by_obj_id[type_id].get(new_task_name) + if new_status is None: + self.log.warning(( + "\"{}\" does not have available status name \"{}\"" + ).format(ent_path, new_task_name)) + continue + try: - task_entity["status_id"] = next_status["id"] + task_entity["status_id"] = new_status["id"] session.commit() self.log.info( "\"{}\" updated status to \"{}\"".format( - ent_path, next_status_name + ent_path, new_status["name"] ) ) except Exception: session.rollback() self.log.warning( "\"{}\" status couldnt be set to \"{}\"".format( - ent_path, next_status_name + ent_path, new_status["name"] ), exc_info=True ) + def statuses_for_tasks(self, task_type_ids, project_entity): + 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 get_sorted_task_type_ids(self, session): types_by_order = collections.defaultdict(list) for _type in session.query("Type").all(): From 5770d7cbd7486ef5ffd83c3120bb7770b754fc40 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 21 Dec 2020 14:03:38 +0100 Subject: [PATCH 04/13] added few more filterings --- pype/modules/ftrack/events/event_next_task_update.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pype/modules/ftrack/events/event_next_task_update.py b/pype/modules/ftrack/events/event_next_task_update.py index 6252985e1b..a26976c16d 100644 --- a/pype/modules/ftrack/events/event_next_task_update.py +++ b/pype/modules/ftrack/events/event_next_task_update.py @@ -188,8 +188,14 @@ class NextTaskUpdate(BaseEvent): continue for task_entity in next_tasks: + if task_entity["status"]["state"]["name"].lower() == "done": + continue + task_status = statuses_by_id[task_entity["status_id"]] old_status_name = task_status["name"].lower() + if old_status_name in ignored_statuses: + continue + new_task_name = mapping.get(old_status_name) if not new_task_name: self.log.debug( From a27d7d335dcc9bc9357d57e6635e35704bdc9bd5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 21 Dec 2020 14:06:23 +0100 Subject: [PATCH 05/13] updated settings gui and values --- .../defaults/project_settings/ftrack.json | 7 +++++-- .../projects_schema/schema_project_ftrack.json | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/pype/settings/defaults/project_settings/ftrack.json b/pype/settings/defaults/project_settings/ftrack.json index 2bf11de468..941c091f5f 100644 --- a/pype/settings/defaults/project_settings/ftrack.json +++ b/pype/settings/defaults/project_settings/ftrack.json @@ -84,8 +84,11 @@ "next_task_update": { "enabled": true, "mapping": { - "Ready": "Not Ready" - } + "Not Ready": "Ready" + }, + "ignored_statuses": [ + "Omitted" + ] } }, "user_handlers": { 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 1554989c55..2625e0062f 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 @@ -302,12 +302,29 @@ "key": "enabled", "label": "Enabled" }, + { + "type": "label", + "label": "Change status on next task by task types order when task status state changed to \"Done\"." + }, + { + "type": "label", + "label": "Mapping of next task status changes From -> To." + }, { "type": "dict-modifiable", "key": "mapping", "object_type": { "type": "text" } + }, + { + "type": "label", + "label": "Status names that are ignored on \"Done\" check (e.g. \"Omitted\")." + }, + { + "type": "list", + "key": "ignored_statuses", + "object_type": "text" } ] } From 5a39ee349e98447be5eb3818939593ad00d4e8cc Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 21 Dec 2020 14:39:50 +0100 Subject: [PATCH 06/13] settings key in class definition --- pype/modules/ftrack/events/event_next_task_update.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pype/modules/ftrack/events/event_next_task_update.py b/pype/modules/ftrack/events/event_next_task_update.py index a26976c16d..14847f8656 100644 --- a/pype/modules/ftrack/events/event_next_task_update.py +++ b/pype/modules/ftrack/events/event_next_task_update.py @@ -3,6 +3,8 @@ from pype.modules.ftrack import BaseEvent class NextTaskUpdate(BaseEvent): + settings_key = "next_task_update" + def launch(self, session, event): '''Propagates status from version to task when changed''' @@ -56,7 +58,7 @@ class NextTaskUpdate(BaseEvent): # Load status mapping from presets event_settings = ( - project_settings["ftrack"]["events"]["next_task_update"] + project_settings["ftrack"]["events"][self.settings_key] ) if not event_settings["enabled"]: self.log.debug("Project \"{}\" has disabled {}.".format( From 254b4c3f8fa8e0984bc149f55fafb5a71d540a03 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Dec 2020 14:09:51 +0100 Subject: [PATCH 07/13] next task update is using new loading of project settings --- pype/modules/ftrack/events/event_next_task_update.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pype/modules/ftrack/events/event_next_task_update.py b/pype/modules/ftrack/events/event_next_task_update.py index 1079681d3c..1f565db85f 100644 --- a/pype/modules/ftrack/events/event_next_task_update.py +++ b/pype/modules/ftrack/events/event_next_task_update.py @@ -47,15 +47,14 @@ class NextTaskUpdate(BaseEvent): return filtered_entities_info def process_by_project(self, session, event, project_id, _entities_info): - project_entity = self.get_project_entity_from_event( + project_name = self.get_project_name_from_event( session, event, project_id ) - project_settings = self.get_settings_for_project( - session, event, project_entity=project_entity + # Load settings + project_settings = self.get_project_settings_from_event( + event, project_name ) - project_name = project_entity["full_name"] - # Load status mapping from presets event_settings = ( project_settings["ftrack"]["events"][self.settings_key] @@ -92,6 +91,7 @@ class NextTaskUpdate(BaseEvent): for task_entity in task_entities: tasks_by_parent_id[task_entity["parent_id"]].append(task_entity) + project_entity = session.get("Project", project_id) self.set_next_task_statuses( session, tasks_by_parent_id, From 26f7df558ad3c3788fbddd14d98300a5e7a22320 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Dec 2020 14:53:59 +0100 Subject: [PATCH 08/13] basic docstring --- .../ftrack/events/event_next_task_update.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pype/modules/ftrack/events/event_next_task_update.py b/pype/modules/ftrack/events/event_next_task_update.py index 1f565db85f..b9ed9abc12 100644 --- a/pype/modules/ftrack/events/event_next_task_update.py +++ b/pype/modules/ftrack/events/event_next_task_update.py @@ -3,6 +3,22 @@ from pype.modules.ftrack import BaseEvent class NextTaskUpdate(BaseEvent): + """Change status on following Task. + + Handler cares about changes of status id on Task entities. When new status + has state `done` it will try to find following task and change it's status. + It is expected following task should be marked as "Ready to work on". + + Handler is based on settings, handler can be turned on/off with "enabled" + key. + Must have set mappings of new statuses: + ``` + "mapping": { + # From -> To + "Not Ready": "Ready" + } + ``` + """ settings_key = "next_task_update" def launch(self, session, event): From 6c1d532a63d31b9068cee6da84fc18f415739eae Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Dec 2020 14:54:11 +0100 Subject: [PATCH 09/13] removed unused variable --- pype/modules/ftrack/events/event_version_to_task_statuses.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pype/modules/ftrack/events/event_version_to_task_statuses.py b/pype/modules/ftrack/events/event_version_to_task_statuses.py index 6a15a697e3..4a42e27336 100644 --- a/pype/modules/ftrack/events/event_version_to_task_statuses.py +++ b/pype/modules/ftrack/events/event_version_to_task_statuses.py @@ -47,8 +47,6 @@ class VersionToTaskStatus(BaseEvent): 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_name = self.get_project_name_from_event( session, event, project_id ) From a7d1fde797d9d7c786ec23ba1416c0f46b5868e3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Dec 2020 18:35:16 +0100 Subject: [PATCH 10/13] added `name_sorting` key to settings under next task update --- .../defaults/project_settings/ftrack.json | 3 ++- .../projects_schema/schema_project_ftrack.json | 17 ++++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/pype/settings/defaults/project_settings/ftrack.json b/pype/settings/defaults/project_settings/ftrack.json index 3801ff5ffc..92c51d0a3b 100644 --- a/pype/settings/defaults/project_settings/ftrack.json +++ b/pype/settings/defaults/project_settings/ftrack.json @@ -88,7 +88,8 @@ }, "ignored_statuses": [ "Omitted" - ] + ], + "name_sorting": false } }, "user_handlers": { 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 2625e0062f..c128d4b751 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 @@ -304,7 +304,7 @@ }, { "type": "label", - "label": "Change status on next task by task types order when task status state changed to \"Done\"." + "label": "Change status on next task by task types order when task status state changed to \"Done\". All tasks with same Task type must be \"Done\"." }, { "type": "label", @@ -317,6 +317,9 @@ "type": "text" } }, + { + "type": "separator" + }, { "type": "label", "label": "Status names that are ignored on \"Done\" check (e.g. \"Omitted\")." @@ -325,6 +328,18 @@ "type": "list", "key": "ignored_statuses", "object_type": "text" + }, + { + "type": "separator" + }, + { + "type": "label", + "label": "Allow to break rule that all tasks with same Task type must be \"Done\" and change statuses with same type tasks ordered by name." + }, + { + "label": "Name sorting", + "type": "boolean", + "key": "name_sorting" } ] } From ae69cb5afc2b781eb266e646ffdd2f9dcb327cb4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Dec 2020 18:38:44 +0100 Subject: [PATCH 11/13] implemented name sorting --- .../ftrack/events/event_next_task_update.py | 60 ++++++++++++++++--- 1 file changed, 53 insertions(+), 7 deletions(-) diff --git a/pype/modules/ftrack/events/event_next_task_update.py b/pype/modules/ftrack/events/event_next_task_update.py index b9ed9abc12..339f951782 100644 --- a/pype/modules/ftrack/events/event_next_task_update.py +++ b/pype/modules/ftrack/events/event_next_task_update.py @@ -172,8 +172,16 @@ class NextTaskUpdate(BaseEvent): type_id = _task_entity["type_id"] task_entities_by_type_id[type_id].append(_task_entity) + if name_sorting: + # Pre sort task entities by name + self.sort_by_name_task_entities_by_type( + task_entities_by_type_id + ) + event_ids = set(event_task_ids_by_parent_id[parent_id]) next_tasks = [] + # `use_next_task` is used only if `name_sorting` is enabled! + use_next_task = False for type_id in sorted_task_type_ids: if type_id not in task_entities_by_type_id: continue @@ -181,7 +189,13 @@ class NextTaskUpdate(BaseEvent): all_in_type_done = True task_entities = task_entities_by_type_id[type_id] if not event_ids: - next_tasks = task_entities + # If `name_sorting` is NOT enabled + if not name_sorting: + next_tasks = task_entities + # If `name_sorting` is enabled and `next_tasks` was not + # filled yet + elif not next_tasks: + next_tasks = [task_entities[0]] break for task_entity in task_entities: @@ -191,13 +205,23 @@ class NextTaskUpdate(BaseEvent): task_status = statuses_by_id[task_entity["status_id"]] low_status_name = task_status["name"].lower() - if low_status_name in ignored_statuses: - continue + if low_status_name not in ignored_statuses: + low_state_name = task_status["state"]["name"].lower() + # Set `next_tasks` if `name_sorting` is enabled and + # all tasks from event were processed + if use_next_task: + # Continue if next task state is set as done + if low_state_name == "done": + continue + next_tasks = [task_entity] + break - low_state_name = task_status["state"]["name"].lower() - if low_state_name != "done": - all_in_type_done = False - break + if low_state_name != "done": + all_in_type_done = False + break + + if name_sorting and not event_ids: + use_next_task = True if not all_in_type_done: break @@ -275,6 +299,28 @@ class NextTaskUpdate(BaseEvent): types.extend(types_by_order[sort_oder]) return types + @staticmethod + def sort_by_name_task_entities_by_type(task_entities_by_type_id): + _task_entities_by_type_id = {} + for type_id, task_entities in task_entities_by_type_id.items(): + # Store tasks by name + task_entities_by_name = {} + for task_entity in task_entities: + task_name = task_entity["name"] + task_entities_by_name[task_name] = task_entity + + # Store task entities by sorted names + sorted_task_entities = [] + for task_name in sorted(task_entities_by_name.keys()): + task_entity = task_entities_by_name[task_name] + sorted_task_entities.append(task_entity) + # Store result to temp dictionary + _task_entities_by_type_id[type_id] = sorted_task_entities + + # Override values in source object + for type_id, value in _task_entities_by_type_id.items(): + task_entities_by_type_id[type_id] = value + def register(session): NextTaskUpdate(session).register() From 9dcbb48c654049fa93c513c4b08d74f01b167114 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Dec 2020 18:39:02 +0100 Subject: [PATCH 12/13] added comments and docstring --- .../ftrack/events/event_next_task_update.py | 42 ++++++++++++++++++- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/pype/modules/ftrack/events/event_next_task_update.py b/pype/modules/ftrack/events/event_next_task_update.py index 339f951782..11a47ace37 100644 --- a/pype/modules/ftrack/events/event_next_task_update.py +++ b/pype/modules/ftrack/events/event_next_task_update.py @@ -6,18 +6,51 @@ class NextTaskUpdate(BaseEvent): """Change status on following Task. Handler cares about changes of status id on Task entities. When new status - has state `done` it will try to find following task and change it's status. + has state "Done" it will try to find following task and change it's status. It is expected following task should be marked as "Ready to work on". + By default all tasks with same task type must have state "Done" to do any + changes. And when all tasks with same task type are "done" it will change + statuses on all tasks with next task type. + + # Enable Handler is based on settings, handler can be turned on/off with "enabled" key. + ``` + "enabled": True + ``` + + # Status mappings Must have set mappings of new statuses: ``` "mapping": { # From -> To - "Not Ready": "Ready" + "Not Ready": "Ready", + ... } ``` + + If current status name is not found then status change is skipped. + + # Ignored statuses + These status names are skipping as they would be in "Done" state. Best + example is status "Omitted" which in most of cases is "Blocked" state but + it will never change. + ``` + "ignored_statuses": [ + "Omitted", + ... + ] + ``` + + # Change statuses sorted by task type and by name + Change behaviour of task type batching. Statuses are not checked and set + by batches of tasks by Task type but one by one. Tasks are sorted by + Task type and then by name if all previous tasks are "Done" the following + will change status. + ``` + "name_sorting": True + ``` """ settings_key = "next_task_update" @@ -146,15 +179,20 @@ class NextTaskUpdate(BaseEvent): for status in statuses } + # Lower ignored statuses ignored_statuses = set( status_name.lower() for status_name in event_settings["ignored_statuses"] ) + # Lower both key and value of mapped statuses mapping = { status_from.lower(): status_to.lower() for status_from, status_to in event_settings["mapping"].items() } + # Should use name sorting or not + name_sorting = event_settings["name_sorting"] + # Collect task type ids from changed entities task_type_ids = set() for task_entities in tasks_by_parent_id.values(): for task_entity in task_entities: From cffe68346c6d224f25f2b02f3b1862b9c6d71c68 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 7 Jan 2021 19:54:12 +0100 Subject: [PATCH 13/13] handler does not care about previous task type ids --- .../ftrack/events/event_next_task_update.py | 250 ++++++++++++------ 1 file changed, 162 insertions(+), 88 deletions(-) diff --git a/pype/modules/ftrack/events/event_next_task_update.py b/pype/modules/ftrack/events/event_next_task_update.py index 11a47ace37..284cff886b 100644 --- a/pype/modules/ftrack/events/event_next_task_update.py +++ b/pype/modules/ftrack/events/event_next_task_update.py @@ -210,108 +210,182 @@ class NextTaskUpdate(BaseEvent): type_id = _task_entity["type_id"] task_entities_by_type_id[type_id].append(_task_entity) + event_ids = set(event_task_ids_by_parent_id[parent_id]) if name_sorting: - # Pre sort task entities by name + # Sort entities by name self.sort_by_name_task_entities_by_type( task_entities_by_type_id ) + # Sort entities by type id + sorted_task_entities = [] + for type_id in sorted_task_type_ids: + task_entities = task_entities_by_type_id.get(type_id) + if task_entities: + sorted_task_entities.extend(task_entities) - event_ids = set(event_task_ids_by_parent_id[parent_id]) - next_tasks = [] - # `use_next_task` is used only if `name_sorting` is enabled! - use_next_task = False - for type_id in sorted_task_type_ids: - if type_id not in task_entities_by_type_id: - continue + next_tasks = self.next_tasks_with_name_sorting( + sorted_task_entities, + event_ids, + statuses_by_id, + ignored_statuses + ) - all_in_type_done = True - task_entities = task_entities_by_type_id[type_id] - if not event_ids: - # If `name_sorting` is NOT enabled - if not name_sorting: - next_tasks = task_entities - # If `name_sorting` is enabled and `next_tasks` was not - # filled yet - elif not next_tasks: - next_tasks = [task_entities[0]] - break + else: + next_tasks = self.next_tasks_with_type_sorting( + task_entities_by_type_id, + sorted_task_type_ids, + event_ids, + statuses_by_id, + ignored_statuses + ) - for task_entity in task_entities: - task_id = task_entity["id"] - if task_id in event_ids: - event_ids.remove(task_id) - - task_status = statuses_by_id[task_entity["status_id"]] - low_status_name = task_status["name"].lower() - if low_status_name not in ignored_statuses: - low_state_name = task_status["state"]["name"].lower() - # Set `next_tasks` if `name_sorting` is enabled and - # all tasks from event were processed - if use_next_task: - # Continue if next task state is set as done - if low_state_name == "done": - continue - next_tasks = [task_entity] - break - - if low_state_name != "done": - all_in_type_done = False - break - - if name_sorting and not event_ids: - use_next_task = True - - if not all_in_type_done: - break - - if not next_tasks: + for task_entity in next_tasks: + if task_entity["status"]["state"]["name"].lower() == "done": continue - for task_entity in next_tasks: - if task_entity["status"]["state"]["name"].lower() == "done": - continue + task_status = statuses_by_id[task_entity["status_id"]] + old_status_name = task_status["name"].lower() + if old_status_name in ignored_statuses: + continue - task_status = statuses_by_id[task_entity["status_id"]] - old_status_name = task_status["name"].lower() - if old_status_name in ignored_statuses: - continue - - new_task_name = mapping.get(old_status_name) - if not new_task_name: - self.log.debug( - "Didn't found mapping for status \"{}\".".format( - task_status["name"] - ) + new_task_name = mapping.get(old_status_name) + if not new_task_name: + self.log.debug( + "Didn't found mapping for status \"{}\".".format( + task_status["name"] ) - continue - - ent_path = "/".join( - [ent["name"] for ent in task_entity["link"]] ) - type_id = task_entity["type_id"] - new_status = statusese_by_obj_id[type_id].get(new_task_name) - if new_status is None: - self.log.warning(( - "\"{}\" does not have available status name \"{}\"" - ).format(ent_path, new_task_name)) + continue + + ent_path = "/".join( + [ent["name"] for ent in task_entity["link"]] + ) + type_id = task_entity["type_id"] + new_status = statusese_by_obj_id[type_id].get(new_task_name) + if new_status is None: + self.log.warning(( + "\"{}\" does not have available status name \"{}\"" + ).format(ent_path, new_task_name)) + continue + + try: + task_entity["status_id"] = new_status["id"] + session.commit() + self.log.info( + "\"{}\" updated status to \"{}\"".format( + ent_path, new_status["name"] + ) + ) + except Exception: + session.rollback() + self.log.warning( + "\"{}\" status couldnt be set to \"{}\"".format( + ent_path, new_status["name"] + ), + exc_info=True + ) + + def next_tasks_with_name_sorting( + self, + sorted_task_entities, + event_ids, + statuses_by_id, + ignored_statuses, + ): + # Pre sort task entities by name + use_next_task = False + next_tasks = [] + for task_entity in sorted_task_entities: + if task_entity["id"] in event_ids: + event_ids.remove(task_entity["id"]) + use_next_task = True + continue + + if not use_next_task: + continue + + task_status = statuses_by_id[task_entity["status_id"]] + low_status_name = task_status["name"].lower() + if low_status_name in ignored_statuses: + continue + + next_tasks.append(task_entity) + use_next_task = False + if not event_ids: + break + + return next_tasks + + def check_statuses_done( + self, task_entities, ignored_statuses, statuses_by_id + ): + all_are_done = True + for task_entity in task_entities: + task_status = statuses_by_id[task_entity["status_id"]] + low_status_name = task_status["name"].lower() + if low_status_name in ignored_statuses: + continue + + low_state_name = task_status["state"]["name"].lower() + if low_state_name != "done": + all_are_done = False + break + return all_are_done + + def next_tasks_with_type_sorting( + self, + task_entities_by_type_id, + sorted_task_type_ids, + event_ids, + statuses_by_id, + ignored_statuses + ): + # `use_next_task` is used only if `name_sorting` is enabled! + next_tasks = [] + use_next_tasks = False + for type_id in sorted_task_type_ids: + if type_id not in task_entities_by_type_id: + continue + + task_entities = task_entities_by_type_id[type_id] + + # Check if any task was in event + event_id_in_tasks = False + for task_entity in task_entities: + task_id = task_entity["id"] + if task_id in event_ids: + event_ids.remove(task_id) + event_id_in_tasks = True + + if use_next_tasks: + # Check if next tasks are not done already + all_in_type_done = self.check_statuses_done( + task_entities, ignored_statuses, statuses_by_id + ) + if all_in_type_done: continue - try: - task_entity["status_id"] = new_status["id"] - session.commit() - self.log.info( - "\"{}\" updated status to \"{}\"".format( - ent_path, new_status["name"] - ) - ) - except Exception: - session.rollback() - self.log.warning( - "\"{}\" status couldnt be set to \"{}\"".format( - ent_path, new_status["name"] - ), - exc_info=True - ) + next_tasks.extend(task_entities) + use_next_tasks = False + if not event_ids: + break + + if not event_id_in_tasks: + continue + + all_in_type_done = self.check_statuses_done( + task_entities, ignored_statuses, statuses_by_id + ) + use_next_tasks = all_in_type_done + if all_in_type_done: + continue + + if not event_ids: + break + + use_next_tasks = False + + return next_tasks def statuses_for_tasks(self, task_type_ids, project_entity): project_schema = project_entity["project_schema"]