From 946821b9fdf59c24a47ae627a5bb0d29ea82026b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 23 Jun 2021 13:16:58 +0200 Subject: [PATCH 01/36] Updated submodule repos/avalon-core --- repos/avalon-core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repos/avalon-core b/repos/avalon-core index efde026e5a..d8be0bdb37 160000 --- a/repos/avalon-core +++ b/repos/avalon-core @@ -1 +1 @@ -Subproject commit efde026e5aad72dac0e69848005419e2c4f067f2 +Subproject commit d8be0bdb37961e32243f1de0eb9696e86acf7443 From 5d651bbc618537c5750d5c55d179b6282fb84249 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 12:52:36 +0200 Subject: [PATCH 02/36] don't add ftrack family in tvpaint collect instances --- openpype/hosts/tvpaint/plugins/publish/collect_instances.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_instances.py b/openpype/hosts/tvpaint/plugins/publish/collect_instances.py index 4468bfae40..e496b144cd 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_instances.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_instances.py @@ -103,8 +103,6 @@ class CollectInstances(pyblish.api.ContextPlugin): instance.data["layers"] = copy.deepcopy( context.data["layersData"] ) - # Add ftrack family - instance.data["families"].append("ftrack") elif family == "renderLayer": instance = self.create_render_layer_instance( @@ -186,9 +184,6 @@ class CollectInstances(pyblish.api.ContextPlugin): instance_data["layers"] = group_layers - # Add ftrack family - instance_data["families"].append("ftrack") - return context.create_instance(**instance_data) def create_render_pass_instance(self, context, instance_data): From 43dca9e537eb8d9ca10c2a3dcd7f981a4165dc8f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 12:52:54 +0200 Subject: [PATCH 03/36] add tvpaint family definition in ftrack collect ftrack family --- .../defaults/project_settings/ftrack.json | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index 03ecf024a6..88f4e1e2e7 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -259,6 +259,26 @@ "tasks": [], "add_ftrack_family": true, "advanced_filtering": [] + }, + { + "hosts": [ + "tvpaint" + ], + "families": [ + "renderPass" + ], + "tasks": [], + "add_ftrack_family": false, + "advanced_filtering": [] + }, + { + "hosts": [ + "tvpaint" + ], + "families": [], + "tasks": [], + "add_ftrack_family": true, + "advanced_filtering": [] } ] }, From 34ca28d856b71dbda31d59b2ff772d5bccd8ce66 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Thu, 24 Jun 2021 13:03:07 +0000 Subject: [PATCH 04/36] [Automated] Bump version --- CHANGELOG.md | 41 ++++++++++++++++++++++++++++------------- openpype/version.py | 2 +- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fe6de4bfa..3fe2ce33cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,44 +1,59 @@ # Changelog -## [3.2.0-nightly.2](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.2.0-nightly.3](https://github.com/pypeclub/OpenPype/tree/HEAD) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/2.18.3...HEAD) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/2.18.4...HEAD) **🚀 Enhancements** +- Settings Hosts enum [\#1739](https://github.com/pypeclub/OpenPype/pull/1739) - Validate containers settings [\#1736](https://github.com/pypeclub/OpenPype/pull/1736) +- PS - added loader from sequence [\#1726](https://github.com/pypeclub/OpenPype/pull/1726) - Autoupdate launcher [\#1725](https://github.com/pypeclub/OpenPype/pull/1725) - Subset template and TVPaint subset template docs [\#1717](https://github.com/pypeclub/OpenPype/pull/1717) +- Toggle Ftrack upload in StandalonePublisher [\#1708](https://github.com/pypeclub/OpenPype/pull/1708) - Overscan color extract review [\#1701](https://github.com/pypeclub/OpenPype/pull/1701) - Nuke: Prerender Frame Range by default [\#1699](https://github.com/pypeclub/OpenPype/pull/1699) - Smoother edges of color triangle [\#1695](https://github.com/pypeclub/OpenPype/pull/1695) **🐛 Bug fixes** +- Backend acre module commit update [\#1745](https://github.com/pypeclub/OpenPype/pull/1745) +- hiero: precollect instances failing when audio selected [\#1743](https://github.com/pypeclub/OpenPype/pull/1743) +- Hiero: creator instance error [\#1742](https://github.com/pypeclub/OpenPype/pull/1742) +- Nuke: fixing render creator for no selection format failing [\#1741](https://github.com/pypeclub/OpenPype/pull/1741) - Local settings UI crash on missing defaults [\#1737](https://github.com/pypeclub/OpenPype/pull/1737) +- TVPaint white background on thumbnail [\#1735](https://github.com/pypeclub/OpenPype/pull/1735) - Ftrack missing custom attribute message [\#1734](https://github.com/pypeclub/OpenPype/pull/1734) - Launcher project changes [\#1733](https://github.com/pypeclub/OpenPype/pull/1733) +- Ftrack sync status [\#1732](https://github.com/pypeclub/OpenPype/pull/1732) - TVPaint use layer name for default variant [\#1724](https://github.com/pypeclub/OpenPype/pull/1724) - Default subset template for TVPaint review and workfile families [\#1716](https://github.com/pypeclub/OpenPype/pull/1716) - Maya: Extract review hotfix [\#1714](https://github.com/pypeclub/OpenPype/pull/1714) - Settings: Imageio improving granularity [\#1711](https://github.com/pypeclub/OpenPype/pull/1711) +- Hiero: published whole edit mov [\#1687](https://github.com/pypeclub/OpenPype/pull/1687) - Application without executables [\#1679](https://github.com/pypeclub/OpenPype/pull/1679) -## [2.18.3](https://github.com/pypeclub/OpenPype/tree/2.18.3) (2021-06-18) +## [2.18.4](https://github.com/pypeclub/OpenPype/tree/2.18.4) (2021-06-24) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/2.18.2...2.18.3) - -**🐛 Bug fixes** - -- Nuke: broken publishing rendered frames [\#1707](https://github.com/pypeclub/OpenPype/pull/1707) -- Remove publish highlight icon in AE [\#1664](https://github.com/pypeclub/OpenPype/pull/1664) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/2.18.3...2.18.4) + +**Merged pull requests:** + +- celaction fixes [\#1754](https://github.com/pypeclub/OpenPype/pull/1754) +- celaciton: audio subset changed data structure [\#1750](https://github.com/pypeclub/OpenPype/pull/1750) + +## [2.18.3](https://github.com/pypeclub/OpenPype/tree/2.18.3) (2021-06-23) + +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.2.0-nightly.2...2.18.3) + +**🐛 Bug fixes** + +- Tools names forwards compatibility [\#1727](https://github.com/pypeclub/OpenPype/pull/1727) **Merged pull requests:** -- Sync main 2.x back to 2.x develop [\#1715](https://github.com/pypeclub/OpenPype/pull/1715) - global: removing obsolete ftrack validator plugin [\#1710](https://github.com/pypeclub/OpenPype/pull/1710) -- \#683 - Validate frame range in Standalone Publisher [\#1680](https://github.com/pypeclub/OpenPype/pull/1680) -- Maya: Split model content validator [\#1654](https://github.com/pypeclub/OpenPype/pull/1654) ## [2.18.2](https://github.com/pypeclub/OpenPype/tree/2.18.2) (2021-06-16) @@ -80,9 +95,9 @@ **🐛 Bug fixes** +- Nuke: broken publishing rendered frames [\#1707](https://github.com/pypeclub/OpenPype/pull/1707) - Standalone publisher Thumbnail export args [\#1705](https://github.com/pypeclub/OpenPype/pull/1705) - Bad zip can break OpenPype start [\#1691](https://github.com/pypeclub/OpenPype/pull/1691) -- Hiero: published whole edit mov [\#1687](https://github.com/pypeclub/OpenPype/pull/1687) - Ftrack subprocess handle of stdout/stderr [\#1675](https://github.com/pypeclub/OpenPype/pull/1675) - Settings list race condifiton and mutable dict list conversion [\#1671](https://github.com/pypeclub/OpenPype/pull/1671) - Mac launch arguments fix [\#1660](https://github.com/pypeclub/OpenPype/pull/1660) diff --git a/openpype/version.py b/openpype/version.py index f527bd4d6e..ce6cfec003 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.2.0-nightly.2" +__version__ = "3.2.0-nightly.3" From a31524b75ced90dfc1f34f9295764347b828d557 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 16:28:08 +0200 Subject: [PATCH 05/36] try to format executable path with environments --- openpype/lib/applications.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index bed57d7022..a7dcb6dd55 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -460,6 +460,12 @@ class ApplicationExecutable: if os.path.exists(_executable): executable = _executable + # Try to format executable with environments + try: + executable = executable.format(**os.environ) + except Exception: + pass + self.executable_path = executable def __str__(self): From 29932c4766dc6711f09f947df42bb1a3703f401a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 16:54:24 +0200 Subject: [PATCH 06/36] pop others before expected keys are processed --- openpype/lib/anatomy.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/lib/anatomy.py b/openpype/lib/anatomy.py index c16c6e2e99..7a4a55363c 100644 --- a/openpype/lib/anatomy.py +++ b/openpype/lib/anatomy.py @@ -733,6 +733,9 @@ class Templates: continue default_key_values[key] = templates.pop(key) + # Pop "others" key before before expected keys are processed + other_templates = templates.pop("others") or {} + keys_by_subkey = {} for sub_key, sub_value in templates.items(): key_values = {} @@ -740,7 +743,6 @@ class Templates: key_values.update(sub_value) keys_by_subkey[sub_key] = cls.prepare_inner_keys(key_values) - other_templates = templates.get("others") or {} for sub_key, sub_value in other_templates.items(): if sub_key in keys_by_subkey: log.warning(( From c74216f082e2b2edd44839daf484dc7e82db73e6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 19:05:50 +0200 Subject: [PATCH 07/36] fix quotes in path for extract thumbnail in standalone publisher --- .../standalonepublisher/plugins/publish/extract_thumbnail.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py index 963d47956a..0792254716 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py @@ -66,7 +66,6 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin): else: # Convert to jpeg if not yet full_input_path = os.path.join(thumbnail_repre["stagingDir"], file) - full_input_path = '"{}"'.format(full_input_path) self.log.info("input {}".format(full_input_path)) full_thumbnail_path = tempfile.mkstemp(suffix=".jpg")[1] From 57bd695974b66f93406926000882d75f2d29794c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 20:05:49 +0200 Subject: [PATCH 08/36] added process_attribute_changes where previous logic happened --- .../event_push_frame_values_to_task.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py index c0b3137455..613566f25d 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py +++ b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py @@ -131,6 +131,18 @@ class PushFrameValuesToTaskEvent(BaseEvent): name_low = object_type["name"].lower() object_types_by_name[name_low] = object_type + if interesting_data: + self.process_attribute_changes( + session, object_types_by_name, + interesting_data, changed_keys_by_object_id, + interest_entity_types, interest_attributes + ) + + def process_attribute_changes( + self, session, object_types_by_name, + interesting_data, changed_keys_by_object_id, + interest_entity_types, interest_attributes + ): # Prepare task object id task_object_id = object_types_by_name["task"]["id"] @@ -216,13 +228,13 @@ class PushFrameValuesToTaskEvent(BaseEvent): task_entity_ids.add(task_id) parent_id_by_task_id[task_id] = task_entity["parent_id"] - self.finalize( + self.finalize_attribute_changes( session, interesting_data, changed_keys, attrs_by_obj_id, hier_attrs, task_entity_ids, parent_id_by_task_id ) - def finalize( + def finalize_attribute_changes( self, session, interesting_data, changed_keys, attrs_by_obj_id, hier_attrs, task_entity_ids, parent_id_by_task_id From 143d1205eedbe5266f02d6ad6a9fecb227919dce Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 20:06:08 +0200 Subject: [PATCH 09/36] collect also task changes if parent_id has changed --- .../event_push_frame_values_to_task.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py index 613566f25d..8c45efa91b 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py +++ b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py @@ -55,10 +55,6 @@ class PushFrameValuesToTaskEvent(BaseEvent): 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: @@ -74,6 +70,14 @@ class PushFrameValuesToTaskEvent(BaseEvent): if project_id is None: continue + # Skip `Task` entity type if parent didn't change + if entity_info["entity_type"].lower() == "task": + if ( + "parent_id" not in changes + or changes["parent_id"]["new"] 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) From a4e84611febe1192d98a21b022a2090764864fad Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 20:06:30 +0200 Subject: [PATCH 10/36] separate task parent changes and value changes --- .../event_push_frame_values_to_task.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py index 8c45efa91b..f0675bdbc8 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py +++ b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py @@ -121,11 +121,21 @@ class PushFrameValuesToTaskEvent(BaseEvent): )) return + # Separate value changes and task parent changes + _entities_info = [] + task_parent_changes = [] + for entity_info in entities_info: + if entity_info["entity_type"].lower() == "task": + task_parent_changes.append(entity_info) + else: + _entities_info.append(entity_info) + entities_info = _entities_info + # 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: + if not interesting_data and not task_parent_changes: return # Prepare object types From 18297588a59349ed41305c9741a7dcde868d5d97 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 20:07:15 +0200 Subject: [PATCH 11/36] convert attributes and types to set --- .../event_handlers_server/event_push_frame_values_to_task.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py index f0675bdbc8..443fdafd71 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py +++ b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py @@ -121,6 +121,9 @@ class PushFrameValuesToTaskEvent(BaseEvent): )) return + interest_attributes = set(self.interest_attributes) + interest_entity_types = set(self.interest_entity_types) + # Separate value changes and task parent changes _entities_info = [] task_parent_changes = [] From 70b91afe199f38652078c1081b36657e9ea8dcba Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 20:15:45 +0200 Subject: [PATCH 12/36] implemented query_custom_attributes for querying custom attribute values from ftrack database --- openpype/modules/ftrack/lib/__init__.py | 4 +- .../modules/ftrack/lib/custom_attributes.py | 57 +++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/lib/__init__.py b/openpype/modules/ftrack/lib/__init__.py index ce6d5284b6..9dc2d67279 100644 --- a/openpype/modules/ftrack/lib/__init__.py +++ b/openpype/modules/ftrack/lib/__init__.py @@ -13,7 +13,8 @@ from .custom_attributes import ( default_custom_attributes_definition, app_definitions_from_app_manager, tool_definitions_from_app_manager, - get_openpype_attr + get_openpype_attr, + query_custom_attributes ) from . import avalon_sync @@ -37,6 +38,7 @@ __all__ = ( "app_definitions_from_app_manager", "tool_definitions_from_app_manager", "get_openpype_attr", + "query_custom_attributes", "avalon_sync", diff --git a/openpype/modules/ftrack/lib/custom_attributes.py b/openpype/modules/ftrack/lib/custom_attributes.py index f6b82c90b1..53facd4ab2 100644 --- a/openpype/modules/ftrack/lib/custom_attributes.py +++ b/openpype/modules/ftrack/lib/custom_attributes.py @@ -81,3 +81,60 @@ def get_openpype_attr(session, split_hierarchical=True, query_keys=None): return custom_attributes, hier_custom_attributes return custom_attributes + + +def join_query_keys(keys): + """Helper to join keys to query.""" + return ",".join(["\"{}\"".format(key) for key in keys]) + + +def query_custom_attributes(session, conf_ids, entity_ids, table_name=None): + """Query custom attribute values from ftrack database. + + Using ftrack call method result may differ based on used table name and + version of ftrack server. + + Args: + session(ftrack_api.Session): Connected ftrack session. + conf_id(list, set, tuple): Configuration(attribute) ids which are + queried. + entity_ids(list, set, tuple): Entity ids for which are values queried. + table_name(str): Table nam from which values are queried. Not + recommended to change until you know what it means. + """ + output = [] + # Just skip + if not conf_ids or not entity_ids: + return output + + if table_name is None: + table_name = "ContextCustomAttributeValue" + + # Prepare values to query + attributes_joined = join_query_keys(conf_ids) + attributes_len = len(conf_ids) + + # Query values in chunks + chunk_size = int(5000 / attributes_len) + # Make sure entity_ids is `list` for chunk selection + entity_ids = list(entity_ids) + for idx in range(0, len(entity_ids), chunk_size): + entity_ids_joined = join_query_keys( + entity_ids[idx:idx + chunk_size] + ) + + call_expr = [{ + "action": "query", + "expression": ( + "select value, entity_id from {}" + " where entity_id in ({}) and configuration_id in ({})" + ).format(table_name, entity_ids_joined, attributes_joined) + }] + if hasattr(session, "call"): + [result] = session.call(call_expr) + else: + [result] = session._call(call_expr) + + for item in result["data"]: + output.append(item) + return output From 0894f272de642c76836e6c90bf01e21c97ceb05a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 20:28:30 +0200 Subject: [PATCH 13/36] implemented _commit_changes for easier access --- .../event_push_frame_values_to_task.py | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py index 443fdafd71..d654b26114 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py +++ b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py @@ -155,6 +155,61 @@ class PushFrameValuesToTaskEvent(BaseEvent): interest_entity_types, interest_attributes ) + def _commit_changes(self, session, changes): + uncommited_changes = False + for idx, item in enumerate(changes): + new_value = item["new_value"] + attr_id = item["attr_id"] + entity_id = item["entity_id"] + attr_key = item["attr_key"] + + entity_key = collections.OrderedDict() + entity_key["configuration_id"] = attr_id + entity_key["entity_id"] = entity_id + self._cached_changes.append({ + "attr_key": attr_key, + "entity_id": entity_id, + "value": new_value, + "time": datetime.datetime.now() + }) + if new_value is None: + op = ftrack_api.operation.DeleteEntityOperation( + "CustomAttributeValue", + entity_key + ) + else: + op = ftrack_api.operation.UpdateEntityOperation( + "ContextCustomAttributeValue", + entity_key, + "value", + ftrack_api.symbol.NOT_SET, + new_value + ) + + session.recorded_operations.push(op) + self.log.info(( + "Changing Custom Attribute \"{}\" to value" + " \"{}\" on entity: {}" + ).format(attr_key, new_value, entity_id)) + + if (idx + 1) % 20 == 0: + uncommited_changes = False + try: + session.commit() + except Exception: + session.rollback() + self.log.warning( + "Changing of values failed.", exc_info=True + ) + else: + uncommited_changes = True + if uncommited_changes: + try: + session.commit() + except Exception: + session.rollback() + self.log.warning("Changing of values failed.", exc_info=True) + def process_attribute_changes( self, session, object_types_by_name, interesting_data, changed_keys_by_object_id, From 3251401da30a216887fa8fe351fb85fcf8d45d86 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 20:29:08 +0200 Subject: [PATCH 14/36] use _commit_changes in current implementation --- .../event_push_frame_values_to_task.py | 37 +++---------------- 1 file changed, 6 insertions(+), 31 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py index d654b26114..c292d856da 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py +++ b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py @@ -332,6 +332,7 @@ class PushFrameValuesToTaskEvent(BaseEvent): session, attr_ids, entity_ids, task_entity_ids, hier_attrs ) + changes = [] for entity_id, current_values in current_values_by_id.items(): parent_id = parent_id_by_task_id.get(entity_id) if not parent_id: @@ -356,39 +357,13 @@ class PushFrameValuesToTaskEvent(BaseEvent): if new_value == old_value: continue - entity_key = collections.OrderedDict() - entity_key["configuration_id"] = attr_id - entity_key["entity_id"] = entity_id - self._cached_changes.append({ - "attr_key": attr_key, + changes.append({ + "new_value": new_value, + "attr_id": attr_id, "entity_id": entity_id, - "value": new_value, - "time": datetime.datetime.now() + "attr_key": attr_key }) - if new_value is None: - op = ftrack_api.operation.DeleteEntityOperation( - "CustomAttributeValue", - entity_key - ) - else: - op = ftrack_api.operation.UpdateEntityOperation( - "ContextCustomAttributeValue", - entity_key, - "value", - ftrack_api.symbol.NOT_SET, - new_value - ) - - session.recorded_operations.push(op) - self.log.info(( - "Changing Custom Attribute \"{}\" to value" - " \"{}\" on entity: {}" - ).format(attr_key, new_value, entity_id)) - try: - session.commit() - except Exception: - session.rollback() - self.log.warning("Changing of values failed.", exc_info=True) + self._commit_changes(session, changes) def filter_changes( self, session, event, entities_info, interest_attributes From 10f0604173d6bfeef2ced3375e97b951fc881fe5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 20:32:09 +0200 Subject: [PATCH 15/36] implemented task parent changes handling --- .../event_push_frame_values_to_task.py | 177 +++++++++++++++++- 1 file changed, 176 insertions(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py index c292d856da..84f26dc57a 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py +++ b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py @@ -2,7 +2,10 @@ import collections import datetime import ftrack_api -from openpype.modules.ftrack.lib import BaseEvent +from openpype.modules.ftrack.lib import ( + BaseEvent, + query_custom_attributes +) class PushFrameValuesToTaskEvent(BaseEvent): @@ -155,6 +158,178 @@ class PushFrameValuesToTaskEvent(BaseEvent): interest_entity_types, interest_attributes ) + if task_parent_changes: + self.process_task_parent_change( + session, object_types_by_name, task_parent_changes, + interest_entity_types, interest_attributes + ) + + def process_task_parent_change( + self, session, object_types_by_name, task_parent_changes, + interest_entity_types, interest_attributes + ): + task_ids = set() + matching_parent_ids = set() + whole_hierarchy_ids = set() + parent_id_by_entity_id = {} + for entity_info in task_parent_changes: + parents = entity_info.get("parents") or [] + # Ignore entities with less parents than 2 + # NOTE entity itself is also part of "parents" value + if len(parents) < 2: + continue + + parent_info = parents[1] + if parent_info["entity_type"] not in interest_entity_types: + continue + + task_ids.add(entity_info["entityId"]) + matching_parent_ids.add(parent_info["entityId"]) + + prev_id = None + for item in parents: + item_id = item["entityId"] + whole_hierarchy_ids.add(item_id) + + if prev_id is None: + prev_id = item_id + continue + + parent_id_by_entity_id[prev_id] = item_id + if item["entityType"] == "show": + break + prev_id = item_id + + if not matching_parent_ids: + return + + entities = session.query( + "select object_type_id from TypedContext where id in ({})".format( + self.join_query_keys(matching_parent_ids) + ) + ) + object_type_ids = set() + for entity in entities: + object_type_ids.add(entity["object_type_id"]) + + # Prepare task object id + task_object_id = object_types_by_name["task"]["id"] + object_type_ids.add(task_object_id) + + attrs_by_obj_id, hier_attrs = self.attrs_configurations( + session, object_type_ids, interest_attributes + ) + + task_attrs = attrs_by_obj_id.get(task_object_id) + if not task_attrs: + return + + for key in interest_attributes: + if key not in hier_attrs: + task_attrs.pop(key, None) + + elif key not in task_attrs: + hier_attrs.pop(key) + + if not task_attrs: + return + + attr_key_by_id = {} + nonhier_id_by_key = {} + hier_attr_ids = [] + for key, attr_id in hier_attrs.items(): + attr_key_by_id[attr_id] = key + hier_attr_ids.append(attr_id) + + conf_ids = list(hier_attr_ids) + for key, attr_id in task_attrs.items(): + attr_key_by_id[attr_id] = key + nonhier_id_by_key[key] = attr_id + conf_ids.append(attr_id) + + result = query_custom_attributes( + session, conf_ids, whole_hierarchy_ids + ) + hier_values_by_entity_id = { + entity_id: {} + for entity_id in whole_hierarchy_ids + } + values_by_entity_id = { + entity_id: { + attr_id: None + for attr_id in conf_ids + } + for entity_id in whole_hierarchy_ids + } + for item in result: + attr_id = item["configuration_id"] + entity_id = item["entity_id"] + value = item["value"] + + values_by_entity_id[entity_id][attr_id] = value + + if attr_id in hier_attr_ids and value is not None: + hier_values_by_entity_id[entity_id][attr_id] = value + + for task_id in tuple(task_ids): + for attr_id in hier_attr_ids: + entity_ids = [] + value = None + entity_id = task_id + while value is None: + entity_value = hier_values_by_entity_id[entity_id] + if attr_id in entity_value: + value = entity_value[attr_id] + if value is None: + break + + if value is None: + entity_ids.append(entity_id) + + entity_id = parent_id_by_entity_id.get(entity_id) + if entity_id is None: + break + + for entity_id in entity_ids: + hier_values_by_entity_id[entity_id][attr_id] = value + + changes = [] + for task_id in tuple(task_ids): + parent_id = parent_id_by_entity_id[task_id] + for attr_id in hier_attr_ids: + attr_key = attr_key_by_id[attr_id] + nonhier_id = nonhier_id_by_key[attr_key] + + # Real value of hierarchical attribute on parent + # - If is none then should be unset + real_parent_value = values_by_entity_id[parent_id][attr_id] + # Current hierarchical value of a task + # - Will be compared to real parent value + hier_value = hier_values_by_entity_id[task_id][attr_id] + + # Parent value that can be inherited from it's parent entity + parent_value = hier_values_by_entity_id[parent_id][attr_id] + # Task value of nonhierarchical custom attribute + nonhier_value = values_by_entity_id[task_id][nonhier_id] + + if real_parent_value != hier_value: + changes.append({ + "new_value": real_parent_value, + "attr_id": attr_id, + "entity_id": task_id, + "attr_key": attr_key + }) + + if parent_value != nonhier_value: + changes.append({ + "new_value": parent_value, + "attr_id": nonhier_id, + "entity_id": task_id, + "attr_key": attr_key + }) + + self._commit_changes(session, changes) + def _commit_changes(self, session, changes): uncommited_changes = False for idx, item in enumerate(changes): From 9fa01ac76ea1b74e76c8cc99195af9e05cefbd97 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 20:43:55 +0200 Subject: [PATCH 16/36] object type ids preparation is at one place --- .../event_push_frame_values_to_task.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py index 84f26dc57a..3ee148b3ed 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py +++ b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py @@ -208,13 +208,13 @@ class PushFrameValuesToTaskEvent(BaseEvent): self.join_query_keys(matching_parent_ids) ) ) - object_type_ids = set() - for entity in entities: - object_type_ids.add(entity["object_type_id"]) # Prepare task object id task_object_id = object_types_by_name["task"]["id"] + object_type_ids = set() object_type_ids.add(task_object_id) + for entity in entities: + object_type_ids.add(entity["object_type_id"]) attrs_by_obj_id, hier_attrs = self.attrs_configurations( session, object_type_ids, interest_attributes From 877b5b853548099a3fad09551e681dc7423d1959 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 20:44:13 +0200 Subject: [PATCH 17/36] added few comments --- .../event_push_frame_values_to_task.py | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py index 3ee148b3ed..d1393796ff 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py +++ b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py @@ -168,24 +168,42 @@ class PushFrameValuesToTaskEvent(BaseEvent): self, session, object_types_by_name, task_parent_changes, interest_entity_types, interest_attributes ): + """Push custom attribute values if task parent has changed. + + Parent is changed if task is created or if is moved under different + entity. We don't care about all task changes only about those that + have it's parent in interest types (from settings). + + Tasks hierarchical value should be unset or set based on parents + real hierarchical value and non hierarchical custom attribute value + should be set to hierarchical value. + """ + # Store task ids which were created or moved under parent with entity + # type defined in settings (interest_entity_types). task_ids = set() + # Store parent ids of matching task ids matching_parent_ids = set() + # Store all entity ids of all entities to be able query hierarchical + # values. whole_hierarchy_ids = set() + # Store parent id of each entity id parent_id_by_entity_id = {} for entity_info in task_parent_changes: - parents = entity_info.get("parents") or [] # Ignore entities with less parents than 2 # NOTE entity itself is also part of "parents" value + parents = entity_info.get("parents") or [] if len(parents) < 2: continue parent_info = parents[1] + # Check if parent has entity type we care about. if parent_info["entity_type"] not in interest_entity_types: continue task_ids.add(entity_info["entityId"]) matching_parent_ids.add(parent_info["entityId"]) + # Store whole hierarchi of task entity prev_id = None for item in parents: item_id = item["entityId"] @@ -200,9 +218,12 @@ class PushFrameValuesToTaskEvent(BaseEvent): break prev_id = item_id + # Just skip if nothing is interesting for our settings if not matching_parent_ids: return + # Query object type ids of parent ids for custom attribute + # definitions query entities = session.query( "select object_type_id from TypedContext where id in ({})".format( self.join_query_keys(matching_parent_ids) @@ -211,6 +232,8 @@ class PushFrameValuesToTaskEvent(BaseEvent): # Prepare task object id task_object_id = object_types_by_name["task"]["id"] + + # All object ids for which we're querying custom attribute definitions object_type_ids = set() object_type_ids.add(task_object_id) for entity in entities: @@ -220,10 +243,13 @@ class PushFrameValuesToTaskEvent(BaseEvent): session, object_type_ids, interest_attributes ) + # Skip if all task attributes are not available task_attrs = attrs_by_obj_id.get(task_object_id) if not task_attrs: return + # Skip attributes that is not in both hierarchical and nonhierarchical + # TODO be able to push values if hierarchical is available for key in interest_attributes: if key not in hier_attrs: task_attrs.pop(key, None) @@ -231,9 +257,11 @@ class PushFrameValuesToTaskEvent(BaseEvent): elif key not in task_attrs: hier_attrs.pop(key) + # Skip if nothing remained if not task_attrs: return + # Do some preparations for custom attribute values query attr_key_by_id = {} nonhier_id_by_key = {} hier_attr_ids = [] @@ -247,13 +275,21 @@ class PushFrameValuesToTaskEvent(BaseEvent): nonhier_id_by_key[key] = attr_id conf_ids.append(attr_id) + # Query custom attribute values + # - result does not contain values for all entities only result of + # query callback to ftrack server result = query_custom_attributes( session, conf_ids, whole_hierarchy_ids ) + + # Prepare variables where result will be stored + # - hierachical values should not contain attribute with value by + # default hier_values_by_entity_id = { entity_id: {} for entity_id in whole_hierarchy_ids } + # - real values of custom attributes values_by_entity_id = { entity_id: { attr_id: None @@ -271,6 +307,10 @@ class PushFrameValuesToTaskEvent(BaseEvent): if attr_id in hier_attr_ids and value is not None: hier_values_by_entity_id[entity_id][attr_id] = value + # Prepare values for all task entities + # - going through all parents and storing first value value + # - store None to those that are already known that do not have set + # value at all for task_id in tuple(task_ids): for attr_id in hier_attr_ids: entity_ids = [] @@ -293,6 +333,7 @@ class PushFrameValuesToTaskEvent(BaseEvent): for entity_id in entity_ids: hier_values_by_entity_id[entity_id][attr_id] = value + # Prepare changes to commit changes = [] for task_id in tuple(task_ids): parent_id = parent_id_by_entity_id[task_id] From 1be4c4fc7f899f2aecec50530f609072bd12edc2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 20:45:33 +0200 Subject: [PATCH 18/36] added some more comments --- .../event_handlers_server/event_push_frame_values_to_task.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py index d1393796ff..1d64174188 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py +++ b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py @@ -151,6 +151,9 @@ class PushFrameValuesToTaskEvent(BaseEvent): name_low = object_type["name"].lower() object_types_by_name[name_low] = object_type + # NOTE it would be nice to check if `interesting_data` do not contain + # value changs of tasks that were created or moved + # - it is a complex way how to find out if interesting_data: self.process_attribute_changes( session, object_types_by_name, From 5cda53abffe2eed91c2bf5c09d8b3a5e559ab702 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 20:58:30 +0200 Subject: [PATCH 19/36] keep refresh button available even if not in dev mode --- openpype/tools/settings/settings/categories.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/openpype/tools/settings/settings/categories.py b/openpype/tools/settings/settings/categories.py index 34ab4c464a..392c749211 100644 --- a/openpype/tools/settings/settings/categories.py +++ b/openpype/tools/settings/settings/categories.py @@ -183,6 +183,12 @@ class SettingsCategoryWidget(QtWidgets.QWidget): footer_widget = QtWidgets.QWidget(configurations_widget) footer_layout = QtWidgets.QHBoxLayout(footer_widget) + refresh_icon = qtawesome.icon("fa.refresh", color="white") + refresh_btn = QtWidgets.QPushButton(footer_widget) + refresh_btn.setIcon(refresh_icon) + + footer_layout.addWidget(refresh_btn, 0) + if self.user_role == "developer": self._add_developer_ui(footer_layout) @@ -205,8 +211,10 @@ class SettingsCategoryWidget(QtWidgets.QWidget): main_layout.addWidget(configurations_widget, 1) save_btn.clicked.connect(self._save) + refresh_btn.clicked.connect(self._on_refresh) self.save_btn = save_btn + self.refresh_btn = refresh_btn self.require_restart_label = require_restart_label self.scroll_widget = scroll_widget self.content_layout = content_layout @@ -220,10 +228,6 @@ class SettingsCategoryWidget(QtWidgets.QWidget): return def _add_developer_ui(self, footer_layout): - refresh_icon = qtawesome.icon("fa.refresh", color="white") - refresh_button = QtWidgets.QPushButton() - refresh_button.setIcon(refresh_icon) - modify_defaults_widget = QtWidgets.QWidget() modify_defaults_checkbox = QtWidgets.QCheckBox(modify_defaults_widget) modify_defaults_checkbox.setChecked(self._hide_studio_overrides) @@ -235,10 +239,8 @@ class SettingsCategoryWidget(QtWidgets.QWidget): modify_defaults_layout.addWidget(label_widget) modify_defaults_layout.addWidget(modify_defaults_checkbox) - footer_layout.addWidget(refresh_button, 0) footer_layout.addWidget(modify_defaults_widget, 0) - refresh_button.clicked.connect(self._on_refresh) modify_defaults_checkbox.stateChanged.connect( self._on_modify_defaults ) From d64e1d249d4eb3042dc89890553d0e536c06c2de Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 25 Jun 2021 11:20:55 +0200 Subject: [PATCH 20/36] width of workfile toos widget have better sizes to display content --- openpype/tools/workfiles/app.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/openpype/tools/workfiles/app.py b/openpype/tools/workfiles/app.py index c79e55a143..d567e26d74 100644 --- a/openpype/tools/workfiles/app.py +++ b/openpype/tools/workfiles/app.py @@ -944,10 +944,8 @@ class Window(QtWidgets.QMainWindow): split_widget.addWidget(tasks_widget) split_widget.addWidget(files_widget) split_widget.addWidget(side_panel) - split_widget.setStretchFactor(0, 1) - split_widget.setStretchFactor(1, 1) - split_widget.setStretchFactor(2, 3) - split_widget.setStretchFactor(3, 1) + split_widget.setSizes([255, 160, 455, 175]) + body_layout.addWidget(split_widget) # Add top margin for tasks to align it visually with files as @@ -976,7 +974,7 @@ class Window(QtWidgets.QMainWindow): # Force focus on the open button by default, required for Houdini. files_widget.btn_open.setFocus() - self.resize(1000, 600) + self.resize(1200, 600) def keyPressEvent(self, event): """Custom keyPressEvent. From 7b273f15497b0980c6a9cf1717882b6ea933188e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 25 Jun 2021 12:37:26 +0200 Subject: [PATCH 21/36] fix project specific environment variables to work as expected --- openpype/lib/applications.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index a7dcb6dd55..9866400928 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -1159,6 +1159,9 @@ def prepare_host_environments(data, implementation_envs=True): def apply_project_environments_value(project_name, env, project_settings=None): """Apply project specific environments on passed environments. + The enviornments are applied on passed `env` argument value so it is not + required to apply changes back. + Args: project_name (str): Name of project for which environemnts should be received. @@ -1167,6 +1170,9 @@ def apply_project_environments_value(project_name, env, project_settings=None): project_settings (dict): Project settings for passed project name. Optional if project settings are already prepared. + Returns: + dict: Passed env values with applied project environments. + Raises: KeyError: If project settings do not contain keys for project specific environments. @@ -1177,10 +1183,9 @@ def apply_project_environments_value(project_name, env, project_settings=None): project_settings = get_project_settings(project_name) env_value = project_settings["global"]["project_environments"] - if not env_value: - return env - parsed = acre.parse(env_value) - return _merge_env(parsed, env) + if env_value: + env.update(_merge_env(acre.parse(env_value), env)) + return env def prepare_context_environments(data): @@ -1209,9 +1214,8 @@ def prepare_context_environments(data): # Load project specific environments project_name = project_doc["name"] - data["env"] = apply_project_environments_value( - project_name, data["env"] - ) + # Apply project specific environments on current env value + apply_project_environments_value(project_name, data["env"]) app = data["app"] workdir_data = get_workdir_data( From 251d3add0162aa3f883daa53fe366158103e843a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 25 Jun 2021 14:51:47 +0200 Subject: [PATCH 22/36] added root_key as abstract property to BaseEntity --- openpype/settings/entities/base_entity.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/settings/entities/base_entity.py b/openpype/settings/entities/base_entity.py index c6bff1ff47..147bd613d1 100644 --- a/openpype/settings/entities/base_entity.py +++ b/openpype/settings/entities/base_entity.py @@ -279,6 +279,11 @@ class BaseItemEntity(BaseEntity): self, "Dynamic entity can't require restart." ) + @abstractproperty + def root_key(self): + """Root is represented as this dictionary key.""" + pass + @abstractmethod def set_override_state(self, state): """Set override state and trigger it on children. From ac36919e2642e27db2fce2042aed61b27f04a061 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 25 Jun 2021 14:52:03 +0200 Subject: [PATCH 23/36] added root_key to both root entities --- openpype/settings/entities/root_entities.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/settings/entities/root_entities.py b/openpype/settings/entities/root_entities.py index 401d3980c9..c637da8f76 100644 --- a/openpype/settings/entities/root_entities.py +++ b/openpype/settings/entities/root_entities.py @@ -491,6 +491,8 @@ class SystemSettings(RootEntity): schema_data (dict): Pass schema data to entity. This is for development and debugging purposes. """ + root_key = SYSTEM_SETTINGS_KEY + def __init__( self, set_studio_state=True, reset=True, schema_data=None ): @@ -600,6 +602,8 @@ class ProjectSettings(RootEntity): schema_data (dict): Pass schema data to entity. This is for development and debugging purposes. """ + root_key = PROJECT_SETTINGS_KEY + def __init__( self, project_name=None, From 81bebe03c675c1a4274a963b5d6653e47667560f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 25 Jun 2021 14:52:16 +0200 Subject: [PATCH 24/36] implemented root_key propery for rest of entities --- openpype/settings/entities/base_entity.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/settings/entities/base_entity.py b/openpype/settings/entities/base_entity.py index 147bd613d1..1b0dd372fa 100644 --- a/openpype/settings/entities/base_entity.py +++ b/openpype/settings/entities/base_entity.py @@ -871,6 +871,10 @@ class ItemEntity(BaseItemEntity): """Call save on root item.""" self.root_item.save() + @property + def root_key(self): + return self.root_item.root_key + def schema_validations(self): if not self.label and self.use_label_wrap: reason = ( From 5c9016c676364097fc3d2de8450bbe95eb45b3d7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 25 Jun 2021 14:52:31 +0200 Subject: [PATCH 25/36] implemented get_entity_from_path for system settings --- openpype/settings/entities/root_entities.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/openpype/settings/entities/root_entities.py b/openpype/settings/entities/root_entities.py index c637da8f76..5ed78fd401 100644 --- a/openpype/settings/entities/root_entities.py +++ b/openpype/settings/entities/root_entities.py @@ -189,11 +189,10 @@ class RootEntity(BaseItemEntity): if not KEY_REGEX.match(key): raise InvalidKeySymbols(self.path, key) + @abstractmethod def get_entity_from_path(self, path): - """Return system settings entity.""" - raise NotImplementedError(( - "Method `get_entity_from_path` not available for \"{}\"" - ).format(self.__class__.__name__)) + """Return entity matching passed path.""" + pass def create_schema_object(self, schema_data, *args, **kwargs): """Create entity by entered schema data. @@ -505,6 +504,18 @@ class SystemSettings(RootEntity): if set_studio_state: self.set_studio_state() + def get_entity_from_path(self, path): + """Return system settings entity.""" + path_parts = path.split("/") + first_part = path_parts[0] + output = self + if first_part == self.root_key: + path_parts.pop(0) + + for path_part in path_parts: + output = output[path_part] + return output + def _reset_values(self): default_value = get_default_settings()[SYSTEM_SETTINGS_KEY] for key, child_obj in self.non_gui_children.items(): From ee72605f5855737b6d7905dd1ec80a393b7caf48 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 25 Jun 2021 14:53:30 +0200 Subject: [PATCH 26/36] implemented copy action in settings --- openpype/tools/settings/settings/base.py | 55 ++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index 03f920b7dc..c0ef968247 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -1,3 +1,5 @@ +import json + from Qt import QtWidgets, QtGui, QtCore from openpype.tools.settings import CHILD_OFFSET from .widgets import ExpandingWidget @@ -125,6 +127,58 @@ class BaseWidget(QtWidgets.QWidget): actions_mapping[action] = remove_from_project_override menu.addAction(action) + def _copy_value_action(self, menu, actions_mapping): + def copy_value(): + mime_data = QtCore.QMimeData() + + if self.entity.is_dynamic_item or self.entity.is_in_dynamic_item: + entity_path = None + else: + entity_path = "/".join( + [self.entity.root_key, self.entity.path] + ) + + value = self.entity.value + # Copy for settings tool + settings_data = { + "root_key": self.entity.root_key, + "value": value, + "path": entity_path + } + settings_encoded_data = QtCore.QByteArray() + settings_stream = QtCore.QDataStream( + settings_encoded_data, QtCore.QIODevice.WriteOnly + ) + settings_stream.writeQString(json.dumps(settings_data)) + mime_data.setData( + "application/copy_settings_value", settings_encoded_data + ) + + # Copy as json + json_encoded_data = None + if isinstance(value, (dict, list)): + json_encoded_data = QtCore.QByteArray() + json_stream = QtCore.QDataStream( + json_encoded_data, QtCore.QIODevice.WriteOnly + ) + json_stream.writeQString(json.dumps(value)) + + mime_data.setData("application/json", json_encoded_data) + + # Copy as text + if json_encoded_data is None: + # Store value as string + mime_data.setText(str(value)) + else: + # Store data as json string + mime_data.setText(json.dumps(value, indent=4)) + + QtWidgets.QApplication.clipboard().setMimeData(mime_data) + + action = QtWidgets.QAction("Copy") + actions_mapping[action] = copy_value + menu.addAction(action) + def show_actions_menu(self, event=None): if event and event.button() != QtCore.Qt.RightButton: return @@ -143,6 +197,7 @@ class BaseWidget(QtWidgets.QWidget): self._remove_from_studio_default_action(menu, actions_mapping) self._add_to_project_override_action(menu, actions_mapping) self._remove_from_project_override_action(menu, actions_mapping) + self._copy_value_action(menu, actions_mapping) if not actions_mapping: action = QtWidgets.QAction("< No action >") From 8eeeda11e7b4058063bd14ab2789e4b9bfad08d7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 25 Jun 2021 14:53:42 +0200 Subject: [PATCH 27/36] implemented base of paste value actions --- openpype/tools/settings/settings/base.py | 41 ++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index c0ef968247..8882cc0c46 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -179,6 +179,46 @@ class BaseWidget(QtWidgets.QWidget): actions_mapping[action] = copy_value menu.addAction(action) + def _paste_value_action(self, menu, actions_mapping): + mime_data = QtWidgets.QApplication.clipboard().mimeData() + mime_value = mime_data.data("application/copy_settings_value") + if not mime_value: + return + + settings_stream = QtCore.QDataStream( + mime_value, QtCore.QIODevice.ReadOnly + ) + mime_data_value_str = settings_stream.readQString() + mime_data_value = json.loads(mime_data_value_str) + + value = mime_data_value["value"] + path = mime_data_value["path"] + root_key = mime_data_value["root_key"] + + def paste_value(): + try: + self.entity.set(value) + except Exception: + # TODO show dialog + print("Failed") + import sys + import traceback + + traceback.print_exception(*sys.exc_info()) + + def paste_value_to_path(): + entity = self.entity.get_entity_from_path(path) + entity.set(value) + + if path and root_key == self.entity.root_key: + action = QtWidgets.QAction("Paste to same entity") + actions_mapping[action] = paste_value_to_path + menu.addAction(action) + + action = QtWidgets.QAction("Paste") + actions_mapping[action] = paste_value + menu.addAction(action) + def show_actions_menu(self, event=None): if event and event.button() != QtCore.Qt.RightButton: return @@ -198,6 +238,7 @@ class BaseWidget(QtWidgets.QWidget): self._add_to_project_override_action(menu, actions_mapping) self._remove_from_project_override_action(menu, actions_mapping) self._copy_value_action(menu, actions_mapping) + self._paste_value_action(menu, actions_mapping) if not actions_mapping: action = QtWidgets.QAction("< No action >") From 2e8df3e2d0339cd1ebf6469af800e26d004527db Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 25 Jun 2021 15:08:29 +0200 Subject: [PATCH 28/36] show dialog if paste crashes --- openpype/tools/settings/settings/base.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index 8882cc0c46..09a36cd99b 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -199,12 +199,14 @@ class BaseWidget(QtWidgets.QWidget): try: self.entity.set(value) except Exception: - # TODO show dialog - print("Failed") - import sys - import traceback - - traceback.print_exception(*sys.exc_info()) + dialog = QtWidgets.QMessageBox(self) + dialog.setWindowTitle("Value does not match settings schema") + dialog.setIcon(QtWidgets.QMessageBox.Warning) + dialog.setText(( + "Pasted value does not seem to match schema of destination" + " settings entity." + )) + dialog.exec_() def paste_value_to_path(): entity = self.entity.get_entity_from_path(path) From 455acfaae9cee59b6a864b1c7fae0732ba55faef Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 25 Jun 2021 15:10:43 +0200 Subject: [PATCH 29/36] paste_value_to_path is simplified --- openpype/tools/settings/settings/base.py | 29 ++++++++++++++++-------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index 09a36cd99b..40a6562fd0 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -195,6 +195,26 @@ class BaseWidget(QtWidgets.QWidget): path = mime_data_value["path"] root_key = mime_data_value["root_key"] + # Try to find matching entity to be able paste values to same spot + # - entity can't by dynamic or in dynamic item + # - must be in same root entity as source copy + # Can't copy system settings <-> project settings + matching_entity = None + if path and root_key == self.entity.root_key: + try: + matching_entity = self.entity.get_entity_from_path(path) + except Exception: + pass + + # Paste value to matchin entity + def paste_value_to_path(): + matching_entity.set(value) + + if matching_entity is not None: + action = QtWidgets.QAction("Paste to same entity", menu) + actions_mapping[action] = paste_value_to_path + menu.addAction(action) + def paste_value(): try: self.entity.set(value) @@ -208,15 +228,6 @@ class BaseWidget(QtWidgets.QWidget): )) dialog.exec_() - def paste_value_to_path(): - entity = self.entity.get_entity_from_path(path) - entity.set(value) - - if path and root_key == self.entity.root_key: - action = QtWidgets.QAction("Paste to same entity") - actions_mapping[action] = paste_value_to_path - menu.addAction(action) - action = QtWidgets.QAction("Paste") actions_mapping[action] = paste_value menu.addAction(action) From 302c6b44b107085616f7522ab91c94fac5da249f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 25 Jun 2021 15:11:18 +0200 Subject: [PATCH 30/36] copy/paste actions are separated with separator in menu --- openpype/tools/settings/settings/base.py | 31 +++++++++++++++--------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index 40a6562fd0..8986780f31 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -127,7 +127,7 @@ class BaseWidget(QtWidgets.QWidget): actions_mapping[action] = remove_from_project_override menu.addAction(action) - def _copy_value_action(self, menu, actions_mapping): + def _copy_value_actions(self, menu): def copy_value(): mime_data = QtCore.QMimeData() @@ -175,15 +175,15 @@ class BaseWidget(QtWidgets.QWidget): QtWidgets.QApplication.clipboard().setMimeData(mime_data) - action = QtWidgets.QAction("Copy") - actions_mapping[action] = copy_value - menu.addAction(action) + action = QtWidgets.QAction("Copy", menu) + return [(action, copy_value)] - def _paste_value_action(self, menu, actions_mapping): + def _paste_value_actions(self, menu): + output = [] mime_data = QtWidgets.QApplication.clipboard().mimeData() mime_value = mime_data.data("application/copy_settings_value") if not mime_value: - return + return output settings_stream = QtCore.QDataStream( mime_value, QtCore.QIODevice.ReadOnly @@ -212,8 +212,7 @@ class BaseWidget(QtWidgets.QWidget): if matching_entity is not None: action = QtWidgets.QAction("Paste to same entity", menu) - actions_mapping[action] = paste_value_to_path - menu.addAction(action) + output.append((action, paste_value_to_path)) def paste_value(): try: @@ -229,8 +228,9 @@ class BaseWidget(QtWidgets.QWidget): dialog.exec_() action = QtWidgets.QAction("Paste") - actions_mapping[action] = paste_value - menu.addAction(action) + output.append((action, paste_value)) + + return output def show_actions_menu(self, event=None): if event and event.button() != QtCore.Qt.RightButton: @@ -250,8 +250,15 @@ class BaseWidget(QtWidgets.QWidget): self._remove_from_studio_default_action(menu, actions_mapping) self._add_to_project_override_action(menu, actions_mapping) self._remove_from_project_override_action(menu, actions_mapping) - self._copy_value_action(menu, actions_mapping) - self._paste_value_action(menu, actions_mapping) + + ui_actions = [] + ui_actions.extend(self._copy_value_actions(menu)) + ui_actions.extend(self._paste_value_actions(menu)) + if ui_actions: + menu.addSeparator() + for action, callback in ui_actions: + menu.addAction(action) + actions_mapping[action] = callback if not actions_mapping: action = QtWidgets.QAction("< No action >") From daf12bb9736ca9c7fae575033c756deb14121b8a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 25 Jun 2021 15:11:23 +0200 Subject: [PATCH 31/36] added comment --- openpype/tools/settings/settings/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index 8986780f31..ac040f9e25 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -214,6 +214,7 @@ class BaseWidget(QtWidgets.QWidget): action = QtWidgets.QAction("Paste to same entity", menu) output.append((action, paste_value_to_path)) + # Simple paste value method def paste_value(): try: self.entity.set(value) From 9c02ecb35aadb1ecae8fb12abf183d4491e839c6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 25 Jun 2021 15:14:56 +0200 Subject: [PATCH 32/36] make both paste secure with dialog poopup --- openpype/tools/settings/settings/base.py | 27 ++++++++++++++---------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index ac040f9e25..620628d1d2 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -180,8 +180,10 @@ class BaseWidget(QtWidgets.QWidget): def _paste_value_actions(self, menu): output = [] + # Allow paste of value only if were copied from this UI mime_data = QtWidgets.QApplication.clipboard().mimeData() mime_value = mime_data.data("application/copy_settings_value") + # Skip if there is nothing to do if not mime_value: return output @@ -206,18 +208,9 @@ class BaseWidget(QtWidgets.QWidget): except Exception: pass - # Paste value to matchin entity - def paste_value_to_path(): - matching_entity.set(value) - - if matching_entity is not None: - action = QtWidgets.QAction("Paste to same entity", menu) - output.append((action, paste_value_to_path)) - - # Simple paste value method - def paste_value(): + def _set_entity_value(_entity, _value): try: - self.entity.set(value) + _entity.set(_value) except Exception: dialog = QtWidgets.QMessageBox(self) dialog.setWindowTitle("Value does not match settings schema") @@ -228,6 +221,18 @@ class BaseWidget(QtWidgets.QWidget): )) dialog.exec_() + # Paste value to matchin entity + def paste_value_to_path(): + _set_entity_value(matching_entity, value) + + if matching_entity is not None: + action = QtWidgets.QAction("Paste to same entity", menu) + output.append((action, paste_value_to_path)) + + # Simple paste value method + def paste_value(): + _set_entity_value(self.entity, value) + action = QtWidgets.QAction("Paste") output.append((action, paste_value)) From 82ac355a3823cd6be24da8e42411748a7c2ee52f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 25 Jun 2021 15:23:39 +0200 Subject: [PATCH 33/36] moved simple Paste before special Paste --- openpype/tools/settings/settings/base.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index 620628d1d2..543551b6a2 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -221,6 +221,13 @@ class BaseWidget(QtWidgets.QWidget): )) dialog.exec_() + # Simple paste value method + def paste_value(): + _set_entity_value(self.entity, value) + + action = QtWidgets.QAction("Paste", menu) + output.append((action, paste_value)) + # Paste value to matchin entity def paste_value_to_path(): _set_entity_value(matching_entity, value) @@ -229,13 +236,6 @@ class BaseWidget(QtWidgets.QWidget): action = QtWidgets.QAction("Paste to same entity", menu) output.append((action, paste_value_to_path)) - # Simple paste value method - def paste_value(): - _set_entity_value(self.entity, value) - - action = QtWidgets.QAction("Paste") - output.append((action, paste_value)) - return output def show_actions_menu(self, event=None): From c86ef80326af322ff9a83f4eb5718497d1fdc471 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 25 Jun 2021 15:23:54 +0200 Subject: [PATCH 34/36] changed label to "Paste to same place" --- openpype/tools/settings/settings/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index 543551b6a2..eb5f82ab9a 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -233,7 +233,7 @@ class BaseWidget(QtWidgets.QWidget): _set_entity_value(matching_entity, value) if matching_entity is not None: - action = QtWidgets.QAction("Paste to same entity", menu) + action = QtWidgets.QAction("Paste to same place", menu) output.append((action, paste_value_to_path)) return output From 9dfbd8d7f2d75ad78201e045c9300eaadc647c79 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 26 Jun 2021 03:40:30 +0000 Subject: [PATCH 35/36] [Automated] Bump version --- CHANGELOG.md | 10 ++++++++-- openpype/version.py | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fe2ce33cb..96b90cd53e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,12 @@ # Changelog -## [3.2.0-nightly.3](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.2.0-nightly.4](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/2.18.4...HEAD) **🚀 Enhancements** +- Application executables with environment variables [\#1757](https://github.com/pypeclub/OpenPype/pull/1757) - Settings Hosts enum [\#1739](https://github.com/pypeclub/OpenPype/pull/1739) - Validate containers settings [\#1736](https://github.com/pypeclub/OpenPype/pull/1736) - PS - added loader from sequence [\#1726](https://github.com/pypeclub/OpenPype/pull/1726) @@ -31,9 +32,12 @@ - Default subset template for TVPaint review and workfile families [\#1716](https://github.com/pypeclub/OpenPype/pull/1716) - Maya: Extract review hotfix [\#1714](https://github.com/pypeclub/OpenPype/pull/1714) - Settings: Imageio improving granularity [\#1711](https://github.com/pypeclub/OpenPype/pull/1711) -- Hiero: published whole edit mov [\#1687](https://github.com/pypeclub/OpenPype/pull/1687) - Application without executables [\#1679](https://github.com/pypeclub/OpenPype/pull/1679) +**Merged pull requests:** + +- TVPaint ftrack family [\#1755](https://github.com/pypeclub/OpenPype/pull/1755) + ## [2.18.4](https://github.com/pypeclub/OpenPype/tree/2.18.4) (2021-06-24) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/2.18.3...2.18.4) @@ -98,6 +102,7 @@ - Nuke: broken publishing rendered frames [\#1707](https://github.com/pypeclub/OpenPype/pull/1707) - Standalone publisher Thumbnail export args [\#1705](https://github.com/pypeclub/OpenPype/pull/1705) - Bad zip can break OpenPype start [\#1691](https://github.com/pypeclub/OpenPype/pull/1691) +- Hiero: published whole edit mov [\#1687](https://github.com/pypeclub/OpenPype/pull/1687) - Ftrack subprocess handle of stdout/stderr [\#1675](https://github.com/pypeclub/OpenPype/pull/1675) - Settings list race condifiton and mutable dict list conversion [\#1671](https://github.com/pypeclub/OpenPype/pull/1671) - Mac launch arguments fix [\#1660](https://github.com/pypeclub/OpenPype/pull/1660) @@ -111,6 +116,7 @@ - update dependencies [\#1697](https://github.com/pypeclub/OpenPype/pull/1697) - Bump normalize-url from 4.5.0 to 4.5.1 in /website [\#1686](https://github.com/pypeclub/OpenPype/pull/1686) +- Use poetry to build / publish OpenPype wheel [\#1636](https://github.com/pypeclub/OpenPype/pull/1636) # Changelog diff --git a/openpype/version.py b/openpype/version.py index ce6cfec003..fcd3b2afca 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.2.0-nightly.3" +__version__ = "3.2.0-nightly.4" From 50cf8f96143eb44612723b9f0e3057be290df9ba Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 14:16:38 +0200 Subject: [PATCH 36/36] fix object attributes error --- .../event_handlers_server/event_push_frame_values_to_task.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py index 1d64174188..81719258e1 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py +++ b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py @@ -124,8 +124,8 @@ class PushFrameValuesToTaskEvent(BaseEvent): )) return - interest_attributes = set(self.interest_attributes) - interest_entity_types = set(self.interest_entity_types) + interest_attributes = set(interest_attributes) + interest_entity_types = set(interest_entity_types) # Separate value changes and task parent changes _entities_info = []