From 98504a205210471242f0c955d3f549561ec392df Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 3 Jun 2022 14:45:05 +0200 Subject: [PATCH 1/6] implemented action that can tranfer values of 1 hierarchical attribute to another --- .../action_translate_hierarchical_values.py | 331 ++++++++++++++++++ 1 file changed, 331 insertions(+) create mode 100644 openpype/modules/ftrack/event_handlers_server/action_translate_hierarchical_values.py diff --git a/openpype/modules/ftrack/event_handlers_server/action_translate_hierarchical_values.py b/openpype/modules/ftrack/event_handlers_server/action_translate_hierarchical_values.py new file mode 100644 index 00000000000..fd10005fadf --- /dev/null +++ b/openpype/modules/ftrack/event_handlers_server/action_translate_hierarchical_values.py @@ -0,0 +1,331 @@ +import copy +import json +import collections + +import ftrack_api + +from openpype_modules.ftrack.lib import ( + ServerAction, + statics_icon, +) +from openpype_modules.ftrack.lib.avalon_sync import create_chunks + + +class TranslateHierarchicalValues(ServerAction): + """Transfer values across hierarhcical attributes. + + Aalso gives ability to convert types meanwhile. That is limited to + conversions between numbers and strings + - int <-> float + - in, float -> string + """ + + identifier = "translate.hierarchical.values" + label = "OpenPype Admin" + variant = "- Translate values between 2 custom attributes" + description = ( + "Move values from a hierarchical attribute to" + " second hierarchical attribute." + ) + icon = statics_icon("ftrack", "action_icons", "OpenPypeAdmin.svg") + + all_project_entities_query = ( + "select id, name, parent_id, link" + " from TypedContext where project_id is \"{}\"" + ) + cust_attr_query = ( + "select value, entity_id from CustomAttributeValue" + " where entity_id in ({}) and configuration_id is \"{}\"" + ) + settings_key = "clean_hierarchical_attr" + + def discover(self, session, entities, event): + """Show anywhere.""" + + return self.valid_roles(session, entities, event) + + def _selection_interface(self, session, event_values=None): + title = "Translate hierarchical values" + + attr_confs = session.query( + ( + "select id, key from CustomAttributeConfiguration" + " where is_hierarchical is true" + ) + ).all() + attr_items = [] + for attr_conf in attr_confs: + attr_items.append({ + "value": attr_conf["id"], + "label": attr_conf["key"] + }) + + if len(attr_items) < 2: + return { + "title": title, + "items": [{ + "type": "label", + "value": ( + "Didn't found custom attributes" + " that can be translated." + ) + }] + } + + attr_items = sorted(attr_items, key=lambda item: item["label"]) + items = [] + item_splitter = {"type": "label", "value": "---"} + items.append({ + "type": "label", + "value": ( + "

Please select source and destination" + " Custom attribute

" + ) + }) + items.append({ + "type": "label", + "value": ( + "WARNING: This will take affect for all projects!" + ) + }) + if event_values: + items.append({ + "type": "label", + "value": ( + "Note: Please select 2 different custom attributes." + ) + }) + + items.append(item_splitter) + + src_item = { + "type": "enumerator", + "label": "Source", + "name": "src_attr_id", + "data": copy.deepcopy(attr_items) + } + dst_item = { + "type": "enumerator", + "label": "Destination", + "name": "dst_attr_id", + "data": copy.deepcopy(attr_items) + } + delete_item = { + "type": "boolean", + "name": "delete_dst_attr_first", + "label": "Delete first", + "value": False + } + if event_values: + src_item["value"] = event_values["src_attr_id"] + dst_item["value"] = event_values["dst_attr_id"] + delete_item["value"] = event_values["delete_dst_attr_first"] + + items.append(src_item) + items.append(dst_item) + items.append(item_splitter) + items.append({ + "type": "label", + "value": ( + "WARNING: All values from destination" + " Custom Attribute will be removed if this is enabled." + ) + }) + items.append(delete_item) + + return { + "title": title, + "items": items + } + + def interface(self, session, entities, event): + if event["data"].get("values", {}): + return None + + return self._selection_interface(session) + + def launch(self, session, entities, event): + values = event["data"].get("values", {}) + if not values: + return None + src_attr_id = values["src_attr_id"] + dst_attr_id = values["dst_attr_id"] + delete_dst_values = values["delete_dst_attr_first"] + + if not src_attr_id or not dst_attr_id: + return { + "success": True, + "message": "Nothing to do" + } + + if src_attr_id == dst_attr_id: + return self._selection_interface(session, values) + + # Query custom attrbutes + src_conf = session.query(( + "select id from CustomAttributeConfiguration where id is {}" + ).format(src_attr_id)).one() + dst_conf = session.query(( + "select id from CustomAttributeConfiguration where id is {}" + ).format(dst_attr_id)).one() + src_type_name = src_conf["type"]["name"] + dst_type_name = dst_conf["type"]["name"] + # Limit conversion to + # - same type -> same type (there is no need to do conversion) + # - number -> number (int to float and back) + # - number -> str (any number can be converted to str) + src_type = None + dst_type = None + if src_type_name == "number" or src_type_name != dst_type_name: + src_type = self._get_attr_type(dst_conf) + dst_type = self._get_attr_type(dst_conf) + valid = False + # Can convert numbers + if src_type in (int, float) and dst_type in (int, float): + valid = True + # Can convert numbers to string + elif dst_type is str: + valid = True + + if not valid: + return { + "message": ( + "Don't know how to properly convert" + " custom attribute types {} > {}" + ).format(src_type_name, dst_type_name), + "success": False + } + + # Query source values + src_attr_values = session.query( + ( + "select value, entity_id" + " from CustomAttributeValue" + " where configuration_id is {}" + ).format(src_attr_id) + ).all() + + value_by_id = {} + failed_entity_ids = [] + for attr_value in src_attr_values: + entity_id = attr_value["entity_id"] + value = attr_value["value"] + if value is not None: + try: + if dst_type is not None: + value = dst_type(value) + value_by_id[entity_id] = value + except Exception: + failed_entity_ids.append(entity_id) + + if failed_entity_ids: + return { + "success": False, + "message": ( + "Couldn't convert some values to destination attribute" + ) + } + + + # Delete destination custom attributes first + if delete_dst_values: + self.log.info("Deleting destination custom attribute values first") + self._delete_custom_attribute_values(session, dst_attr_id) + + self._apply_values(session, value_by_id, dst_attr_id) + return True + + def _delete_custom_attribute_values(self, session, dst_attr_id): + dst_attr_values = session.query( + ( + "select configuration_id, entity_id" + " from CustomAttributeValue" + " where configuration_id is {}" + ).format(dst_attr_id) + ).all() + delete_operations = [] + for attr_value in dst_attr_values: + entity_id = attr_value["entity_id"] + configuration_id = attr_value["configuration_id"] + entity_key = collections.OrderedDict(( + ("configuration_id", configuration_id), + ("entity_id", entity_id) + )) + delete_operations.append( + ftrack_api.operation.DeleteEntityOperation( + "CustomAttributeValue", + entity_key + ) + ) + + if not delete_operations: + return + + for chunk in create_chunks(delete_operations, 500): + for operation in chunk: + session.recorded_operations.push(operation) + session.commit() + + def _apply_values(self, session, value_by_id, dst_attr_id): + dst_attr_values = session.query( + ( + "select configuration_id, entity_id" + " from CustomAttributeValue" + " where configuration_id is {}" + ).format(dst_attr_id) + ).all() + + dst_entity_ids_with_value = { + item["entity_id"] + for item in dst_attr_values + } + operations = [] + for entity_id, value in value_by_id.items(): + entity_key = collections.OrderedDict(( + ("configuration_id", dst_attr_id), + ("entity_id", entity_id) + )) + if entity_id in dst_entity_ids_with_value: + operations.append( + ftrack_api.operation.UpdateEntityOperation( + "CustomAttributeValue", + entity_key, + "value", + ftrack_api.symbol.NOT_SET, + value + ) + ) + else: + operations.append( + ftrack_api.operation.CreateEntityOperation( + "CustomAttributeValue", + entity_key, + {"value": value} + ) + ) + + if not operations: + return + + for chunk in create_chunks(operations, 500): + for operation in chunk: + session.recorded_operations.push(operation) + session.commit() + + def _get_attr_type(self, conf_def): + type_name = conf_def["type"]["name"] + if type_name == "text": + return str + + if type_name == "number": + config = json.loads(conf_def["config"]) + if config["isdecimal"]: + return float + return int + return None + + +def register(session): + '''Register plugin. Called when used as an plugin.''' + + TranslateHierarchicalValues(session).register() From 8e7358a33b9da4a97681a1dae4119e30b238a24a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 3 Jun 2022 14:51:21 +0200 Subject: [PATCH 2/6] changed translate to transfer --- .../action_translate_hierarchical_values.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/action_translate_hierarchical_values.py b/openpype/modules/ftrack/event_handlers_server/action_translate_hierarchical_values.py index fd10005fadf..8cc6fa3a573 100644 --- a/openpype/modules/ftrack/event_handlers_server/action_translate_hierarchical_values.py +++ b/openpype/modules/ftrack/event_handlers_server/action_translate_hierarchical_values.py @@ -11,7 +11,7 @@ from openpype_modules.ftrack.lib.avalon_sync import create_chunks -class TranslateHierarchicalValues(ServerAction): +class TransferHierarchicalValues(ServerAction): """Transfer values across hierarhcical attributes. Aalso gives ability to convert types meanwhile. That is limited to @@ -20,9 +20,9 @@ class TranslateHierarchicalValues(ServerAction): - in, float -> string """ - identifier = "translate.hierarchical.values" + identifier = "transfer.hierarchical.values" label = "OpenPype Admin" - variant = "- Translate values between 2 custom attributes" + variant = "- Transfer values between 2 custom attributes" description = ( "Move values from a hierarchical attribute to" " second hierarchical attribute." @@ -37,7 +37,7 @@ class TranslateHierarchicalValues(ServerAction): "select value, entity_id from CustomAttributeValue" " where entity_id in ({}) and configuration_id is \"{}\"" ) - settings_key = "clean_hierarchical_attr" + settings_key = "transfer_values_of_hierarchical_attributes" def discover(self, session, entities, event): """Show anywhere.""" @@ -45,7 +45,7 @@ def discover(self, session, entities, event): return self.valid_roles(session, entities, event) def _selection_interface(self, session, event_values=None): - title = "Translate hierarchical values" + title = "Transfer hierarchical values" attr_confs = session.query( ( @@ -67,7 +67,7 @@ def _selection_interface(self, session, event_values=None): "type": "label", "value": ( "Didn't found custom attributes" - " that can be translated." + " that can be transfered." ) }] } @@ -328,4 +328,4 @@ def _get_attr_type(self, conf_def): def register(session): '''Register plugin. Called when used as an plugin.''' - TranslateHierarchicalValues(session).register() + TransferHierarchicalValues(session).register() From 3da8536f968c932811a7979680cc27980594c2fe Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 3 Jun 2022 14:51:32 +0200 Subject: [PATCH 3/6] added settings for new action --- .../defaults/project_settings/ftrack.json | 7 +++++++ .../schema_project_ftrack.json | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index f9d16d64761..9d59deea3d6 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -109,6 +109,13 @@ "Omitted" ], "name_sorting": false + }, + "transfer_values_of_hierarchical_attributes": { + "enabled": true, + "role_list": [ + "Administrator", + "Project manager" + ] } }, "user_handlers": { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json index 7db490b1144..16cab49d5d0 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json @@ -369,6 +369,25 @@ "key": "name_sorting" } ] + }, + { + "type": "dict", + "key": "transfer_values_of_hierarchical_attributes", + "label": "Action to transfer hierarchical attribute values", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "list", + "key": "role_list", + "label": "Roles", + "object_type": "text" + } + ] } ] }, From d37497df10d6883f3b154f076ce91e7163c9d89b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 3 Jun 2022 14:54:33 +0200 Subject: [PATCH 4/6] changed filename --- ...erarchical_values.py => action_tranfer_hierarchical_values.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename openpype/modules/ftrack/event_handlers_server/{action_translate_hierarchical_values.py => action_tranfer_hierarchical_values.py} (100%) diff --git a/openpype/modules/ftrack/event_handlers_server/action_translate_hierarchical_values.py b/openpype/modules/ftrack/event_handlers_server/action_tranfer_hierarchical_values.py similarity index 100% rename from openpype/modules/ftrack/event_handlers_server/action_translate_hierarchical_values.py rename to openpype/modules/ftrack/event_handlers_server/action_tranfer_hierarchical_values.py From 078a47e32f39b02c4c6c5db8860c2c7a6de886b5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 3 Jun 2022 14:58:59 +0200 Subject: [PATCH 5/6] added some logs --- .../action_tranfer_hierarchical_values.py | 38 +++++++++++++------ 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/action_tranfer_hierarchical_values.py b/openpype/modules/ftrack/event_handlers_server/action_tranfer_hierarchical_values.py index 8cc6fa3a573..9df3b67969b 100644 --- a/openpype/modules/ftrack/event_handlers_server/action_tranfer_hierarchical_values.py +++ b/openpype/modules/ftrack/event_handlers_server/action_tranfer_hierarchical_values.py @@ -153,12 +153,17 @@ def launch(self, session, entities, event): delete_dst_values = values["delete_dst_attr_first"] if not src_attr_id or not dst_attr_id: + self.log.info("Attributes were not filled. Nothing to do.") return { "success": True, "message": "Nothing to do" } if src_attr_id == dst_attr_id: + self.log.info(( + "Same attributes were selected {}, {}." + " Showing interface again." + ).format(src_attr_id, dst_attr_id)) return self._selection_interface(session, values) # Query custom attrbutes @@ -188,6 +193,10 @@ def launch(self, session, entities, event): valid = True if not valid: + self.log.info(( + "Don't know how to properly convert" + " custom attribute types {} > {}" + ).format(src_type_name, dst_type_name)) return { "message": ( "Don't know how to properly convert" @@ -205,20 +214,26 @@ def launch(self, session, entities, event): ).format(src_attr_id) ).all() - value_by_id = {} + self.log.debug("Queried source values.") failed_entity_ids = [] - for attr_value in src_attr_values: - entity_id = attr_value["entity_id"] - value = attr_value["value"] - if value is not None: - try: - if dst_type is not None: - value = dst_type(value) - value_by_id[entity_id] = value - except Exception: - failed_entity_ids.append(entity_id) + if dst_type is not None: + self.log.debug("Converting source values to desctination type") + value_by_id = {} + for attr_value in src_attr_values: + entity_id = attr_value["entity_id"] + value = attr_value["value"] + if value is not None: + try: + if dst_type is not None: + value = dst_type(value) + value_by_id[entity_id] = value + except Exception: + failed_entity_ids.append(entity_id) if failed_entity_ids: + self.log.info( + "Couldn't convert some values to destination attribute" + ) return { "success": False, "message": ( @@ -232,6 +247,7 @@ def launch(self, session, entities, event): self.log.info("Deleting destination custom attribute values first") self._delete_custom_attribute_values(session, dst_attr_id) + self.log.info("Applying source values on destination custom attribute") self._apply_values(session, value_by_id, dst_attr_id) return True From 9a4f33532f308033d4c589e71a5687cd87be0700 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 3 Jun 2022 15:04:20 +0200 Subject: [PATCH 6/6] hound fix --- .../event_handlers_server/action_tranfer_hierarchical_values.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/modules/ftrack/event_handlers_server/action_tranfer_hierarchical_values.py b/openpype/modules/ftrack/event_handlers_server/action_tranfer_hierarchical_values.py index 9df3b67969b..d160b7200db 100644 --- a/openpype/modules/ftrack/event_handlers_server/action_tranfer_hierarchical_values.py +++ b/openpype/modules/ftrack/event_handlers_server/action_tranfer_hierarchical_values.py @@ -241,7 +241,6 @@ def launch(self, session, entities, event): ) } - # Delete destination custom attributes first if delete_dst_values: self.log.info("Deleting destination custom attribute values first")