From 3faee05cf6c05e089d8be8d42708493e7114933b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 18 Aug 2025 13:28:24 +0200 Subject: [PATCH 01/40] Added toggle for keeping original names of publishes --- .../ayon_core/tools/push_to_project/control.py | 10 ++++++---- .../tools/push_to_project/ui/window.py | 18 +++++++++++++++++- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/control.py b/client/ayon_core/tools/push_to_project/control.py index b52eeb5fad..5e1f758d79 100644 --- a/client/ayon_core/tools/push_to_project/control.py +++ b/client/ayon_core/tools/push_to_project/control.py @@ -40,6 +40,8 @@ class PushToContextController: self.set_source(project_name, version_id) + self._use_original_name = False + # Events system def emit_event(self, topic, data=None, source=None): @@ -315,6 +317,8 @@ class PushToContextController: return product_name def _check_submit_validations(self): + if self._use_original_name: + return True if not self._user_values.is_valid: return False @@ -339,10 +343,8 @@ class PushToContextController: ) def _submit_callback(self): - process_item_id = self._process_item_id - if process_item_id is None: - return - self._integrate_model.integrate_item(process_item_id) + for process_item_id in self._process_item_ids: + self._integrate_model.integrate_item(process_item_id) self._emit_event("submit.finished", {}) if process_item_id == self._process_item_id: self._process_item_id = None diff --git a/client/ayon_core/tools/push_to_project/ui/window.py b/client/ayon_core/tools/push_to_project/ui/window.py index 38c343b023..1f40958a66 100644 --- a/client/ayon_core/tools/push_to_project/ui/window.py +++ b/client/ayon_core/tools/push_to_project/ui/window.py @@ -133,6 +133,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget): inputs_widget = QtWidgets.QWidget(main_splitter) new_folder_checkbox = NiceCheckbox(True, parent=inputs_widget) + original_names_checkbox = NiceCheckbox(False, parent=inputs_widget) folder_name_input = PlaceholderLineEdit(inputs_widget) folder_name_input.setPlaceholderText("< Name of new folder >") @@ -151,6 +152,8 @@ class PushToContextSelectWindow(QtWidgets.QWidget): inputs_layout.addRow("Create new folder", new_folder_checkbox) inputs_layout.addRow("New folder name", folder_name_input) inputs_layout.addRow("Variant", variant_input) + inputs_layout.addRow( + "Use original product names", original_names_checkbox) inputs_layout.addRow("Comment", comment_input) main_splitter.addWidget(context_widget) @@ -250,6 +253,8 @@ class PushToContextSelectWindow(QtWidgets.QWidget): variant_input.textChanged.connect(self._on_variant_change) comment_input.textChanged.connect(self._on_comment_change) library_only_checkbox.stateChanged.connect(self._on_library_only_change) + original_names_checkbox.stateChanged.connect( + self._on_original_names_change) publish_btn.clicked.connect(self._on_select_click) cancel_btn.clicked.connect(self._on_close_click) @@ -408,8 +413,15 @@ class PushToContextSelectWindow(QtWidgets.QWidget): """Change toggle state, reset filter, recalculate dropdown""" state = bool(state) self._projects_combobox.set_standard_filter_enabled(state) - self._projects_combobox.refresh() + def _on_original_names_change(self, state: int) -> None: + use_original_name = bool(state) + self._new_folder_name_enabled = not use_original_name + self._new_folder_checkbox.setEnabled(not use_original_name) + self._folder_name_input.setEnabled(not use_original_name) + self._variant_input.setEnabled(not use_original_name) + self._controller._use_original_name = use_original_name + self.refresh() def _on_user_input_timer(self): folder_name_enabled = self._new_folder_name_enabled @@ -466,6 +478,8 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self._header_label.setText(self._controller.get_source_label()) def _invalidate_new_folder_name(self, folder_name, is_valid): + if self._controller._use_original_name: + is_valid = True self._tasks_widget.setVisible(folder_name is None) if self._folder_is_valid is is_valid: return @@ -478,6 +492,8 @@ class PushToContextSelectWindow(QtWidgets.QWidget): ) def _invalidate_variant(self, is_valid): + if self._controller._use_original_name: + is_valid = True if self._variant_is_valid is is_valid: return self._variant_is_valid = is_valid From ef9ab3bcdc106854472608a5a0772888ed128089 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 18 Aug 2025 13:31:31 +0200 Subject: [PATCH 02/40] Changed main input to accept multiple version ids --- client/ayon_core/tools/push_to_project/main.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/main.py b/client/ayon_core/tools/push_to_project/main.py index a6ff38c16f..3a80dc2bb2 100644 --- a/client/ayon_core/tools/push_to_project/main.py +++ b/client/ayon_core/tools/push_to_project/main.py @@ -4,28 +4,28 @@ from ayon_core.tools.utils import get_ayon_qt_app from ayon_core.tools.push_to_project.ui import PushToContextSelectWindow -def main_show(project_name, version_id): +def main_show(project_name, version_ids): app = get_ayon_qt_app() window = PushToContextSelectWindow() window.show() - window.set_source(project_name, version_id) + window.set_source(project_name, version_ids) app.exec_() @click.command() @click.option("--project", help="Source project name") -@click.option("--version", help="Source version id") -def main(project, version): +@click.option("--versions", help="Source version ids") +def main(project, versions): """Run PushToProject tool to integrate version in different project. Args: project (str): Source project name. - version (str): Version id. + versions (str): comma separated versions for same context """ - main_show(project, version) + main_show(project, versions) if __name__ == "__main__": From 965f937e28022e6154e4f2f5ace34e429580a764 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 18 Aug 2025 13:32:15 +0200 Subject: [PATCH 03/40] Implemented loader action to push multiple versions --- client/ayon_core/plugins/load/push_to_library.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/client/ayon_core/plugins/load/push_to_library.py b/client/ayon_core/plugins/load/push_to_library.py index 22c10bbad7..42a63a8625 100644 --- a/client/ayon_core/plugins/load/push_to_library.py +++ b/client/ayon_core/plugins/load/push_to_library.py @@ -28,25 +28,22 @@ class PushToLibraryProject(load.ProductLoaderPlugin): if not filtered_contexts: raise LoadError("Nothing to push for your selection") - if len(filtered_contexts) > 1: - raise LoadError("Please select only one item") - - context = tuple(filtered_contexts)[0] - push_tool_script_path = os.path.join( AYON_CORE_ROOT, "tools", "push_to_project", "main.py" ) + project_name = tuple(filtered_contexts)[0]["project"]["name"] - project_name = context["project"]["name"] - version_id = context["version"]["id"] + version_ids = [] + for context in filtered_contexts: + version_ids.append(context["version"]["id"]) args = get_ayon_launcher_args( "run", push_tool_script_path, "--project", project_name, - "--version", version_id + "--versions", ",".join(version_ids) ) run_detached_process(args) From 073f8bfec58f4ab97d04ee74e4f88b20857e87ec Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 18 Aug 2025 13:35:05 +0200 Subject: [PATCH 04/40] Implemented processing of multiple items --- .../tools/push_to_project/control.py | 121 ++++++++++-------- .../tools/push_to_project/ui/window.py | 46 ++++--- 2 files changed, 100 insertions(+), 67 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/control.py b/client/ayon_core/tools/push_to_project/control.py index 5e1f758d79..88031d2a8a 100644 --- a/client/ayon_core/tools/push_to_project/control.py +++ b/client/ayon_core/tools/push_to_project/control.py @@ -27,11 +27,11 @@ class PushToContextController: self._user_values = UserPublishValuesModel(self) self._src_project_name = None - self._src_version_id = None + self._src_version_ids = [] self._src_folder_entity = None self._src_folder_task_entities = {} - self._src_product_entity = None - self._src_version_entity = None + self._src_product_entities = [] + self._src_version_entities = [] self._src_label = None self._submission_enabled = False @@ -54,38 +54,43 @@ class PushToContextController: def register_event_callback(self, topic, callback): self._event_system.add_callback(topic, callback) - def set_source(self, project_name, version_id): + def set_source(self, project_name, version_ids): """Set source project and version. Args: project_name (Union[str, None]): Source project name. - version_id (Union[str, None]): Source version id. + version_id (Union[str, None]): Comma separated source version ids. """ if ( project_name == self._src_project_name - and version_id == self._src_version_id + and version_ids == self._src_version_ids ): return self._src_project_name = project_name - self._src_version_id = version_id + self._src_version_ids = version_ids.split(",") self._src_label = None folder_entity = None task_entities = {} - product_entity = None - version_entity = None - if project_name and version_id: - version_entity = ayon_api.get_version_by_id( - project_name, version_id + product_entities = None + version_entities = None + if project_name and self._src_version_ids: + version_entities = list(ayon_api.get_versions( + project_name, version_ids=self._src_version_ids)) + + if version_entities: + product_ids = [ + version_entity["productId"] + for version_entity in version_entities + ] + product_entities = list(ayon_api.get_products( + project_name, product_ids=product_ids) ) - if version_entity: - product_entity = ayon_api.get_product_by_id( - project_name, version_entity["productId"] - ) - - if product_entity: + if product_entities: + # all products for same folder + product_entity = product_entities[0] folder_entity = ayon_api.get_folder_by_id( project_name, product_entity["folderId"] ) @@ -100,15 +105,15 @@ class PushToContextController: self._src_folder_entity = folder_entity self._src_folder_task_entities = task_entities - self._src_product_entity = product_entity - self._src_version_entity = version_entity - if folder_entity: + self._src_product_entities = product_entities + self._src_version_entities = version_entities + if folder_entity and len(list(version_entities)) == 1: self._user_values.set_new_folder_name(folder_entity["name"]) variant = self._get_src_variant() if variant: self._user_values.set_variant(variant) - comment = version_entity["attrib"].get("comment") + comment = version_entities[0]["attrib"].get("comment") if comment: self._user_values.set_comment(comment) @@ -116,7 +121,7 @@ class PushToContextController: "source.changed", { "project_name": project_name, - "version_id": version_id + "version_ids": self._src_version_ids } ) @@ -179,29 +184,32 @@ class PushToContextController: if self._process_thread is not None: return - item_id = self._integrate_model.create_process_item( - self._src_project_name, - self._src_version_id, - self._selection_model.get_selected_project_name(), - self._selection_model.get_selected_folder_id(), - self._selection_model.get_selected_task_name(), - self._user_values.variant, - comment=self._user_values.comment, - new_folder_name=self._user_values.new_folder_name, - dst_version=1 - ) + item_ids = [] + for src_version_entity in self._src_version_entities: + item_id = self._integrate_model.create_process_item( + self._src_project_name, + src_version_entity["id"], + self._selection_model.get_selected_project_name(), + self._selection_model.get_selected_folder_id(), + self._selection_model.get_selected_task_name(), + self._user_values.variant, + comment=self._user_values.comment, + new_folder_name=self._user_values.new_folder_name, + dst_version=1, + ) + item_ids.append(item_id) - self._process_item_id = item_id + self._process_item_ids = item_ids self._emit_event("submit.started") if wait: self._submit_callback() - self._process_item_id = None + self._process_item_ids = [] return item_id thread = threading.Thread(target=self._submit_callback) self._process_thread = thread thread.start() - return item_id + return item_ids def wait_for_process_thread(self): if self._process_thread is None: @@ -210,22 +218,34 @@ class PushToContextController: self._process_thread = None def _prepare_source_label(self): - if not self._src_project_name or not self._src_version_id: + if not self._src_project_name or not self._src_version_ids: return "Source is not defined" folder_entity = self._src_folder_entity if not folder_entity: return "Source is invalid" + no_of_products = len(self._src_product_entities) + no_of_versions = len(self._src_version_entities) + if no_of_products != no_of_versions: + return (f"Not matching number of products {no_of_products} and " + f"versions {no_of_versions}") + folder_path = folder_entity["path"] - product_entity = self._src_product_entity - version_entity = self._src_version_entity - return "Source: {}{}/{}/v{:0>3}".format( - self._src_project_name, - folder_path, - product_entity["name"], - version_entity["version"] - ) + src_labels = [] + for idx in range(0, no_of_versions): + product_entity = self._src_product_entities[idx] + version_entity = self._src_version_entities[idx] + src_labels.append( + "Source: {}{}/{}/v{:0>3}".format( + self._src_project_name, + folder_path, + product_entity["name"], + version_entity["version"], + ) + ) + + return "\n".join(src_labels) def _get_task_info_from_repre_entities( self, task_entities, repre_entities @@ -258,8 +278,9 @@ class PushToContextController: return None, None def _get_src_variant(self): + """Could be triggered only if single version is moved.""" project_name = self._src_project_name - version_entity = self._src_version_entity + version_entity = self._src_version_entities[0] task_entities = self._src_folder_task_entities repre_entities = ayon_api.get_representations( project_name, version_ids={version_entity["id"]} @@ -269,7 +290,7 @@ class PushToContextController: ) project_settings = get_project_settings(project_name) - product_type = self._src_product_entity["productType"] + product_type = self._src_product_entities[0]["productType"] template = get_product_name_template( self._src_project_name, product_type, @@ -303,7 +324,7 @@ class PushToContextController: print("Failed format", exc) return "" - product_name = self._src_product_entity["name"] + product_name = self._src_product_entities[0]["name"] if ( (product_s and not product_name.startswith(product_s)) or (product_e and not product_name.endswith(product_e)) @@ -346,8 +367,6 @@ class PushToContextController: for process_item_id in self._process_item_ids: self._integrate_model.integrate_item(process_item_id) self._emit_event("submit.finished", {}) - if process_item_id == self._process_item_id: - self._process_item_id = None def _emit_event(self, topic, data=None): if data is None: diff --git a/client/ayon_core/tools/push_to_project/ui/window.py b/client/ayon_core/tools/push_to_project/ui/window.py index 1f40958a66..147191e659 100644 --- a/client/ayon_core/tools/push_to_project/ui/window.py +++ b/client/ayon_core/tools/push_to_project/ui/window.py @@ -331,7 +331,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self._main_thread_timer = main_thread_timer self._main_thread_timer_can_stop = True self._last_submit_message = None - self._process_item_id = None + self._process_item_ids = [] self._variant_is_valid = None self._folder_is_valid = None @@ -342,17 +342,17 @@ class PushToContextSelectWindow(QtWidgets.QWidget): overlay_try_btn.setVisible(False) # Support of public api function of controller - def set_source(self, project_name, version_id): + def set_source(self, project_name, version_ids): """Set source project and version. Call the method on controller. Args: project_name (Union[str, None]): Name of project. - version_id (Union[str, None]): Version id. + version_id (Union[str, None]): Version ids. """ - self._controller.set_source(project_name, version_id) + self._controller.set_source(project_name, version_ids) def showEvent(self, event): super(PushToContextSelectWindow, self).showEvent(event) @@ -528,31 +528,45 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self._overlay_label.setText(self._last_submit_message) self._last_submit_message = None - process_status = self._controller.get_process_item_status( - self._process_item_id - ) - push_failed = process_status["failed"] - fail_traceback = process_status["full_traceback"] + failed_pushes = [] + fail_tracebacks = [] + for process_item_id in self._process_item_ids: + process_status = self._controller.get_process_item_status( + process_item_id + ) + if process_status["failed"]: + failed_pushes.append(process_status) + # push_failed = process_status["failed"] + # fail_traceback = process_status["full_traceback"] if self._main_thread_timer_can_stop: self._main_thread_timer.stop() self._overlay_close_btn.setVisible(True) - if push_failed: + if failed_pushes: self._overlay_try_btn.setVisible(True) - if fail_traceback: + fail_tracebacks = [ + process_status["full_traceback"] + for process_status in failed_pushes + if process_status["full_traceback"] + ] + if fail_tracebacks: self._show_detail_btn.setVisible(True) - if push_failed: - reason = process_status["fail_reason"] - if fail_traceback: + if failed_pushes: + reasons = [ + process_status["fail_reason"] + for process_status in failed_pushes + ] + if fail_tracebacks: + reason = "\n".join(reasons) message = ( "Unhandled error happened." " Check error detail for more information." ) self._error_detail_dialog.set_detail( - reason, fail_traceback + reason, "\n".join(fail_tracebacks) ) else: - message = f"Push Failed:\n{reason}" + message = f"Push Failed:\n{reasons}" self._overlay_label.setText(message) set_style_property(self._overlay_close_btn, "state", "error") From c7c28e1153777d12f4c69b3f76095fcd2bb667df Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 18 Aug 2025 13:37:03 +0200 Subject: [PATCH 05/40] Pushed use_original_name to ProjectPushItem --- .../tools/push_to_project/control.py | 1 + .../tools/push_to_project/models/integrate.py | 66 +++++++++++-------- 2 files changed, 39 insertions(+), 28 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/control.py b/client/ayon_core/tools/push_to_project/control.py index 88031d2a8a..483efdd22d 100644 --- a/client/ayon_core/tools/push_to_project/control.py +++ b/client/ayon_core/tools/push_to_project/control.py @@ -196,6 +196,7 @@ class PushToContextController: comment=self._user_values.comment, new_folder_name=self._user_values.new_folder_name, dst_version=1, + use_original_name=self._use_original_name, ) item_ids.append(item_id) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index b180892d62..c66c74219c 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -90,6 +90,7 @@ class ProjectPushItem: new_folder_name, dst_version, item_id=None, + use_original_name=False ): if not item_id: item_id = uuid.uuid4().hex @@ -104,6 +105,7 @@ class ProjectPushItem: self.comment = comment or "" self.item_id = item_id self._repr_value = None + self.use_original_name = use_original_name @property def _repr(self): @@ -115,7 +117,8 @@ class ProjectPushItem: str(self.dst_folder_id), str(self.new_folder_name), str(self.dst_task_name), - str(self.dst_version) + str(self.dst_version), + self.use_original_name ]) return self._repr_value @@ -134,6 +137,7 @@ class ProjectPushItem: "comment": self.comment, "new_folder_name": self.new_folder_name, "item_id": self.item_id, + "use_original_name": self.use_original_name } @classmethod @@ -373,7 +377,7 @@ class ProjectPushRepreItem: resource_files.append(ResourceFile(filepath, relative_path)) continue - filepath = os.path.join(src_dirpath, basename) + # filepath = os.path.join(src_dirpath, basename) frame = None udim = None for item in src_basename_regex.finditer(basename): @@ -819,31 +823,34 @@ class ProjectPushItemProcess: self._template_name = template_name def _determine_product_name(self): - product_type = self._product_type - task_info = self._task_info - task_name = task_type = None - if task_info: - task_name = task_info["name"] - task_type = task_info["taskType"] + if self._item.use_original_name: + product_name = self._src_product_entity["name"] + else: + product_type = self._product_type + task_info = self._task_info + task_name = task_type = None + if task_info: + task_name = task_info["name"] + task_type = task_info["taskType"] - try: - product_name = get_product_name( - self._item.dst_project_name, - task_name, - task_type, - self.host_name, - product_type, - self._item.variant, - project_settings=self._project_settings - ) - except TaskNotSetError: - self._status.set_failed( - "Target product name template requires task name. To continue" - " you have to select target task or change settings" - " ayon+settings://core/tools/creator/product_name_profiles" - f"?project={self._item.dst_project_name}." - ) - raise PushToProjectError(self._status.fail_reason) + try: + product_name = get_product_name( + self._item.dst_project_name, + task_name, + task_type, + self.host_name, + product_type, + self._item.variant, + project_settings=self._project_settings + ) + except TaskNotSetError: + self._status.set_failed( + "Target product name template requires task name. To " + "continue you have to select target task or change settings " + " ayon+settings://core/tools/creator/product_name_profiles" + f"?project={self._item.dst_project_name}." + ) + raise PushToProjectError(self._status.fail_reason) self._log_info( f"Push will be integrating to product with name '{product_name}'" @@ -1137,7 +1144,7 @@ class ProjectPushItemProcess: self._item.dst_project_name, "representation", entity_id, - changes + changes, ) existing_repre_names = set(existing_repres_by_low_name.keys()) @@ -1196,6 +1203,7 @@ class IntegrateModel: comment, new_folder_name, dst_version, + use_original_name ): """Create new item for integration. @@ -1209,6 +1217,7 @@ class IntegrateModel: comment (Union[str, None]): Comment. new_folder_name (Union[str, None]): New folder name. dst_version (int): Destination version number. + use_original_name (bool): If original product names should be used Returns: str: Item id. The id can be used to trigger integration or get @@ -1224,7 +1233,8 @@ class IntegrateModel: variant, comment=comment, new_folder_name=new_folder_name, - dst_version=dst_version + dst_version=dst_version, + use_original_name=use_original_name ) process_item = ProjectPushItemProcess(self, item) self._process_items[item.item_id] = process_item From 4a44570799f6d5a020a72b6cef60446163782600 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 18 Aug 2025 13:37:36 +0200 Subject: [PATCH 06/40] Invalidate all input fields after refresh --- client/ayon_core/tools/push_to_project/ui/window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/push_to_project/ui/window.py b/client/ayon_core/tools/push_to_project/ui/window.py index 147191e659..d07488e719 100644 --- a/client/ayon_core/tools/push_to_project/ui/window.py +++ b/client/ayon_core/tools/push_to_project/ui/window.py @@ -370,7 +370,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self._invalidate_new_folder_name( new_folder_name, user_values["is_new_folder_name_valid"] ) - + self._controller._invalidate() self._projects_combobox.refresh() def _on_first_show(self): From 8586431f17ac6503f604e7c1cf9be440f6483e6b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 18 Aug 2025 15:34:54 +0200 Subject: [PATCH 07/40] Fix version id argument --- client/ayon_core/tools/push_to_project/control.py | 9 +++++---- client/ayon_core/tools/push_to_project/ui/window.py | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/control.py b/client/ayon_core/tools/push_to_project/control.py index 1ccda9440d..b90e938cf3 100644 --- a/client/ayon_core/tools/push_to_project/control.py +++ b/client/ayon_core/tools/push_to_project/control.py @@ -16,7 +16,7 @@ from .models import ( class PushToContextController: - def __init__(self, project_name=None, version_id=None): + def __init__(self, project_name=None, version_ids=None): self._event_system = self._create_event_system() self._projects_model = ProjectsModel(self) @@ -38,7 +38,7 @@ class PushToContextController: self._process_thread = None self._process_item_id = None - self.set_source(project_name, version_id) + self.set_source(project_name, version_ids) self._use_original_name = False @@ -58,9 +58,10 @@ class PushToContextController: Args: project_name (Union[str, None]): Source project name. - version_id (Union[str, None]): Comma separated source version ids. + version_ids (Union[str, None]): Comma separated source version ids. """ - + if not project_name or not version_ids: + return if ( project_name == self._src_project_name and version_ids == self._src_version_ids diff --git a/client/ayon_core/tools/push_to_project/ui/window.py b/client/ayon_core/tools/push_to_project/ui/window.py index d07488e719..3fb1822d92 100644 --- a/client/ayon_core/tools/push_to_project/ui/window.py +++ b/client/ayon_core/tools/push_to_project/ui/window.py @@ -349,7 +349,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget): Args: project_name (Union[str, None]): Name of project. - version_id (Union[str, None]): Version ids. + version_ids (Union[str, None]): comma separated Version ids. """ self._controller.set_source(project_name, version_ids) From cb09825b8b36802b443d1ad5a06ac250361ca004 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 18 Aug 2025 16:33:57 +0200 Subject: [PATCH 08/40] Removed set_source in init --- client/ayon_core/tools/push_to_project/control.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/control.py b/client/ayon_core/tools/push_to_project/control.py index b90e938cf3..d02cd4dfc0 100644 --- a/client/ayon_core/tools/push_to_project/control.py +++ b/client/ayon_core/tools/push_to_project/control.py @@ -38,8 +38,6 @@ class PushToContextController: self._process_thread = None self._process_item_id = None - self.set_source(project_name, version_ids) - self._use_original_name = False # Events system From 12f415a639781b16c299e4b16edf128e9381e1a7 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 18 Aug 2025 17:01:42 +0200 Subject: [PATCH 09/40] Added tooltip --- client/ayon_core/tools/push_to_project/ui/window.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/ayon_core/tools/push_to_project/ui/window.py b/client/ayon_core/tools/push_to_project/ui/window.py index 3fb1822d92..b58904a31a 100644 --- a/client/ayon_core/tools/push_to_project/ui/window.py +++ b/client/ayon_core/tools/push_to_project/ui/window.py @@ -208,6 +208,10 @@ class PushToContextSelectWindow(QtWidgets.QWidget): show_detail_btn.setToolTip( "Show error detail dialog to copy full error." ) + original_names_checkbox.setToolTip( + "Required for multi copy, doesn't allow changes in folder or " + "variant values." + ) overlay_close_btn = QtWidgets.QPushButton( "Close", overlay_btns_widget From e5bf5d3070ea27d8b4f7755cf64f5d0967a500b3 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 20 Aug 2025 10:13:21 +0200 Subject: [PATCH 10/40] Split versions directly in main Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/tools/push_to_project/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/push_to_project/main.py b/client/ayon_core/tools/push_to_project/main.py index 3a80dc2bb2..d3c9d3a537 100644 --- a/client/ayon_core/tools/push_to_project/main.py +++ b/client/ayon_core/tools/push_to_project/main.py @@ -25,7 +25,7 @@ def main(project, versions): versions (str): comma separated versions for same context """ - main_show(project, versions) + main_show(project, versions.split(",")) if __name__ == "__main__": From 26ab3671039ea6ea68198288a384d514058c6426 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 20 Aug 2025 10:13:43 +0200 Subject: [PATCH 11/40] Keep set_source Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/tools/push_to_project/control.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/ayon_core/tools/push_to_project/control.py b/client/ayon_core/tools/push_to_project/control.py index d02cd4dfc0..42f45ae500 100644 --- a/client/ayon_core/tools/push_to_project/control.py +++ b/client/ayon_core/tools/push_to_project/control.py @@ -39,6 +39,8 @@ class PushToContextController: self._process_item_id = None self._use_original_name = False + + self.set_source(project_name, version_ids) # Events system def emit_event(self, topic, data=None, source=None): From cd7b6212ccbf2fc48f2bfdbbeafd160d34e1d288 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 20 Aug 2025 10:13:59 +0200 Subject: [PATCH 12/40] Update docstrign Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/tools/push_to_project/control.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/push_to_project/control.py b/client/ayon_core/tools/push_to_project/control.py index 42f45ae500..c1c5a1bd37 100644 --- a/client/ayon_core/tools/push_to_project/control.py +++ b/client/ayon_core/tools/push_to_project/control.py @@ -58,7 +58,7 @@ class PushToContextController: Args: project_name (Union[str, None]): Source project name. - version_ids (Union[str, None]): Comma separated source version ids. + version_ids (Optional[list[str]]): Version ids. """ if not project_name or not version_ids: return From 015e7c11a500957392c0bb4e0c0adce9421a48fe Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 20 Aug 2025 10:14:17 +0200 Subject: [PATCH 13/40] Split done before Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/tools/push_to_project/control.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/push_to_project/control.py b/client/ayon_core/tools/push_to_project/control.py index c1c5a1bd37..cbcfb75157 100644 --- a/client/ayon_core/tools/push_to_project/control.py +++ b/client/ayon_core/tools/push_to_project/control.py @@ -69,7 +69,7 @@ class PushToContextController: return self._src_project_name = project_name - self._src_version_ids = version_ids.split(",") + self._src_version_ids = version_ids self._src_label = None folder_entity = None task_entities = {} From 2bea321e9b34c2a48ce94e9912c1a4ecb38eeaad Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 20 Aug 2025 10:14:39 +0200 Subject: [PATCH 14/40] Update initializations Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/tools/push_to_project/control.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/control.py b/client/ayon_core/tools/push_to_project/control.py index cbcfb75157..d28cb17c98 100644 --- a/client/ayon_core/tools/push_to_project/control.py +++ b/client/ayon_core/tools/push_to_project/control.py @@ -73,8 +73,8 @@ class PushToContextController: self._src_label = None folder_entity = None task_entities = {} - product_entities = None - version_entities = None + product_entities = [] + version_entities = [] if project_name and self._src_version_ids: version_entities = list(ayon_api.get_versions( project_name, version_ids=self._src_version_ids)) From 0574fa46b8f37d3f04823125e8c503617ad50695 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 20 Aug 2025 10:27:18 +0200 Subject: [PATCH 15/40] Fix formatting --- client/ayon_core/tools/push_to_project/control.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/push_to_project/control.py b/client/ayon_core/tools/push_to_project/control.py index d28cb17c98..9d5a1cb90c 100644 --- a/client/ayon_core/tools/push_to_project/control.py +++ b/client/ayon_core/tools/push_to_project/control.py @@ -39,7 +39,7 @@ class PushToContextController: self._process_item_id = None self._use_original_name = False - + self.set_source(project_name, version_ids) # Events system From e4305cc37a095511f89b87c791136b9c212a5168 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 20 Aug 2025 10:30:23 +0200 Subject: [PATCH 16/40] Removed product_entities Used only on 2 places --- .../tools/push_to_project/control.py | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/control.py b/client/ayon_core/tools/push_to_project/control.py index 9d5a1cb90c..666a9a94a2 100644 --- a/client/ayon_core/tools/push_to_project/control.py +++ b/client/ayon_core/tools/push_to_project/control.py @@ -30,7 +30,6 @@ class PushToContextController: self._src_version_ids = [] self._src_folder_entity = None self._src_folder_task_entities = {} - self._src_product_entities = [] self._src_version_entities = [] self._src_label = None @@ -105,7 +104,6 @@ class PushToContextController: self._src_folder_entity = folder_entity self._src_folder_task_entities = task_entities - self._src_product_entities = product_entities self._src_version_entities = version_entities if folder_entity and len(list(version_entities)) == 1: self._user_values.set_new_folder_name(folder_entity["name"]) @@ -226,17 +224,13 @@ class PushToContextController: if not folder_entity: return "Source is invalid" - no_of_products = len(self._src_product_entities) - no_of_versions = len(self._src_version_entities) - if no_of_products != no_of_versions: - return (f"Not matching number of products {no_of_products} and " - f"versions {no_of_versions}") - folder_path = folder_entity["path"] src_labels = [] - for idx in range(0, no_of_versions): - product_entity = self._src_product_entities[idx] - version_entity = self._src_version_entities[idx] + for version_entity in self._src_version_entities: + product_entity = ayon_api.get_product_by_id( + self._src_project_name, + version_entity["productId"] + ) src_labels.append( "Source: {}{}/{}/v{:0>3}".format( self._src_project_name, @@ -289,9 +283,13 @@ class PushToContextController: task_name, task_type = self._get_task_info_from_repre_entities( task_entities, repre_entities ) + product_entity = ayon_api.get_product_by_id( + project_name, + version_entity["productId"] + ) project_settings = get_project_settings(project_name) - product_type = self._src_product_entities[0]["productType"] + product_type = product_entity["productType"] template = get_product_name_template( self._src_project_name, product_type, @@ -325,7 +323,7 @@ class PushToContextController: print("Failed format", exc) return "" - product_name = self._src_product_entities[0]["name"] + product_name = product_entity["name"] if ( (product_s and not product_name.startswith(product_s)) or (product_e and not product_name.endswith(product_e)) From cbc227ae2df9920339fc8a423c33f4eb69910ccc Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 20 Aug 2025 10:42:51 +0200 Subject: [PATCH 17/40] Parse variant and folder name even for multi push --- client/ayon_core/tools/push_to_project/control.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/control.py b/client/ayon_core/tools/push_to_project/control.py index 666a9a94a2..c661c05d5d 100644 --- a/client/ayon_core/tools/push_to_project/control.py +++ b/client/ayon_core/tools/push_to_project/control.py @@ -105,7 +105,7 @@ class PushToContextController: self._src_folder_entity = folder_entity self._src_folder_task_entities = task_entities self._src_version_entities = version_entities - if folder_entity and len(list(version_entities)) == 1: + if folder_entity: self._user_values.set_new_folder_name(folder_entity["name"]) variant = self._get_src_variant() if variant: @@ -273,8 +273,8 @@ class PushToContextController: return None, None def _get_src_variant(self): - """Could be triggered only if single version is moved.""" project_name = self._src_project_name + # parse variant only from first version version_entity = self._src_version_entities[0] task_entities = self._src_folder_task_entities repre_entities = ayon_api.get_representations( From 1e9d6997731e7bf0ec72b9a855d93f316c763a04 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 20 Aug 2025 10:50:26 +0200 Subject: [PATCH 18/40] Allow folder create even if Use original name --- client/ayon_core/tools/push_to_project/ui/window.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/ui/window.py b/client/ayon_core/tools/push_to_project/ui/window.py index b58904a31a..d63b2582e4 100644 --- a/client/ayon_core/tools/push_to_project/ui/window.py +++ b/client/ayon_core/tools/push_to_project/ui/window.py @@ -209,7 +209,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget): "Show error detail dialog to copy full error." ) original_names_checkbox.setToolTip( - "Required for multi copy, doesn't allow changes in folder or " + "Required for multi copy, doesn't allow changes " "variant values." ) @@ -420,9 +420,6 @@ class PushToContextSelectWindow(QtWidgets.QWidget): def _on_original_names_change(self, state: int) -> None: use_original_name = bool(state) - self._new_folder_name_enabled = not use_original_name - self._new_folder_checkbox.setEnabled(not use_original_name) - self._folder_name_input.setEnabled(not use_original_name) self._variant_input.setEnabled(not use_original_name) self._controller._use_original_name = use_original_name self.refresh() @@ -482,8 +479,6 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self._header_label.setText(self._controller.get_source_label()) def _invalidate_new_folder_name(self, folder_name, is_valid): - if self._controller._use_original_name: - is_valid = True self._tasks_widget.setVisible(folder_name is None) if self._folder_is_valid is is_valid: return From 13e1cc71030323e6afb1040adcb172e6aae70ede Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 21 Aug 2025 11:33:51 +0200 Subject: [PATCH 19/40] Fix project_name Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/plugins/load/push_to_project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/load/push_to_project.py b/client/ayon_core/plugins/load/push_to_project.py index aff3efd6f6..6d641f2a57 100644 --- a/client/ayon_core/plugins/load/push_to_project.py +++ b/client/ayon_core/plugins/load/push_to_project.py @@ -34,7 +34,7 @@ class PushToProject(load.ProductLoaderPlugin): "push_to_project", "main.py" ) - project_name = tuple(filtered_contexts)[0]["project"]["name"] + project_name = filtered_contexts[0]["project"]["name"] version_ids = [] for context in filtered_contexts: From 60e6d4df2f3ba0d819cb867f05ed23efdecdd77c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 21 Aug 2025 11:34:19 +0200 Subject: [PATCH 20/40] Update logic for versions Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/plugins/load/push_to_project.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/plugins/load/push_to_project.py b/client/ayon_core/plugins/load/push_to_project.py index 6d641f2a57..d5dd8960a3 100644 --- a/client/ayon_core/plugins/load/push_to_project.py +++ b/client/ayon_core/plugins/load/push_to_project.py @@ -36,9 +36,10 @@ class PushToProject(load.ProductLoaderPlugin): ) project_name = filtered_contexts[0]["project"]["name"] - version_ids = [] - for context in filtered_contexts: - version_ids.append(context["version"]["id"]) + version_ids = { + context["version"]["id"] + for context in filtered_contexts + } args = get_ayon_launcher_args( push_tool_script_path, From bab05592bc242d7ca1f4a1913e19342b8af646f9 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 21 Aug 2025 11:35:13 +0200 Subject: [PATCH 21/40] Update when even is emitted Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/tools/push_to_project/control.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/push_to_project/control.py b/client/ayon_core/tools/push_to_project/control.py index c661c05d5d..6247fe14ce 100644 --- a/client/ayon_core/tools/push_to_project/control.py +++ b/client/ayon_core/tools/push_to_project/control.py @@ -363,10 +363,15 @@ class PushToContextController: ) def _submit_callback(self): - for process_item_id in self._process_item_ids: + process_item_ids = self._process_item_ids + for process_item_id in process_item_ids: self._integrate_model.integrate_item(process_item_id) + self._emit_event("submit.finished", {}) + if process_item_ids is self._process_item_ids: + self._process_item_ids = [] + def _emit_event(self, topic, data=None): if data is None: data = {} From 641d7879820c4b07f4be49ba5303582b19188d99 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 21 Aug 2025 11:36:40 +0200 Subject: [PATCH 22/40] Reordered input fields validations --- client/ayon_core/tools/push_to_project/control.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/control.py b/client/ayon_core/tools/push_to_project/control.py index 6247fe14ce..ea01165859 100644 --- a/client/ayon_core/tools/push_to_project/control.py +++ b/client/ayon_core/tools/push_to_project/control.py @@ -337,11 +337,6 @@ class PushToContextController: return product_name def _check_submit_validations(self): - if self._use_original_name: - return True - if not self._user_values.is_valid: - return False - if not self._selection_model.get_selected_project_name(): return False @@ -350,6 +345,13 @@ class PushToContextController: and not self._selection_model.get_selected_folder_id() ): return False + + if self._use_original_name: + return True + + if not self._user_values.is_valid: + return False + return True def _invalidate(self): From 0933e882e200f09cb194dc847df8e55247056c1c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 21 Aug 2025 13:45:21 +0200 Subject: [PATCH 23/40] Fix wrong repre["context"] content Contained only values used in resolving template. Missed project["name"] etc. --- .../tools/push_to_project/models/integrate.py | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index f9d524ba3a..c888adf733 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -1019,10 +1019,18 @@ class ProjectPushItemProcess: self, anatomy, template_name, formatting_data, file_template ): processed_repre_items = [] + repre_context = None for repre_item in self._src_repre_items: repre_entity = repre_item.repre_entity repre_name = repre_entity["name"] repre_format_data = copy.deepcopy(formatting_data) + + if not repre_context: + repre_context = self._update_repre_context( + repre_entity, + formatting_data + ) + repre_format_data["representation"] = repre_name for src_file in repre_item.src_files: ext = os.path.splitext(src_file.path)[-1] @@ -1038,7 +1046,6 @@ class ProjectPushItemProcess: "publish", template_name, "directory" ) folder_path = template_obj.format_strict(formatting_data) - repre_context = folder_path.used_values folder_path_rootless = folder_path.rootless repre_filepaths = [] published_path = None @@ -1061,7 +1068,6 @@ class ProjectPushItemProcess: ) if published_path is None or frame == repre_item.frame: published_path = dst_filepath - repre_context.update(filename.used_values) repre_filepaths.append((dst_filepath, dst_rootless_path)) self._file_transaction.add(src_file.path, dst_filepath) @@ -1178,6 +1184,28 @@ class ProjectPushItemProcess: path ) + def _update_repre_context(self, repre_entity, formatting_data): + """Replace old context value with new ones. + + Folder might change, project definitely changes etc. + """ + repre_context = repre_entity["context"] + for context_key, context_value in repre_context.items(): + if context_value and isinstance(context_value, dict): + for context_sub_key in context_value.keys(): + value_to_update = formatting_data.get(context_key, {}).get( + context_sub_key) + if value_to_update: + repre_context[context_key][ + context_sub_key] = value_to_update + else: + value_to_update = formatting_data.get(context_key) + if value_to_update: + repre_context[context_key] = value_to_update + if "task" not in formatting_data: + repre_context.pop("task") + return repre_context + class IntegrateModel: def __init__(self, controller): From b032d26ec6d417c0d31bfe74c6cb65bc60b50f21 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 21 Aug 2025 13:57:16 +0200 Subject: [PATCH 24/40] Formatting change --- client/ayon_core/tools/push_to_project/control.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/push_to_project/control.py b/client/ayon_core/tools/push_to_project/control.py index ea01165859..58d06dd19d 100644 --- a/client/ayon_core/tools/push_to_project/control.py +++ b/client/ayon_core/tools/push_to_project/control.py @@ -368,7 +368,7 @@ class PushToContextController: process_item_ids = self._process_item_ids for process_item_id in process_item_ids: self._integrate_model.integrate_item(process_item_id) - + self._emit_event("submit.finished", {}) if process_item_ids is self._process_item_ids: From 3f941a1dff82cce941a7c1f4ef6523441daf43d1 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 21 Aug 2025 15:08:09 +0200 Subject: [PATCH 25/40] Fix where to pull process_item_ids --- client/ayon_core/tools/push_to_project/ui/window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/push_to_project/ui/window.py b/client/ayon_core/tools/push_to_project/ui/window.py index d63b2582e4..4d947103be 100644 --- a/client/ayon_core/tools/push_to_project/ui/window.py +++ b/client/ayon_core/tools/push_to_project/ui/window.py @@ -529,7 +529,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget): failed_pushes = [] fail_tracebacks = [] - for process_item_id in self._process_item_ids: + for process_item_id in self._controller._process_item_ids: process_status = self._controller.get_process_item_status( process_item_id ) From c7d0f2b9871c59f334ce062d57c0d73af0256675 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 21 Aug 2025 15:08:44 +0200 Subject: [PATCH 26/40] Add more logging to exception handling --- client/ayon_core/tools/push_to_project/models/integrate.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index c888adf733..054a5f1b18 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -497,8 +497,11 @@ class ProjectPushItemProcess: except Exception as exc: _exc, _value, _tb = sys.exc_info() + product_name = self._src_product_entity["name"] self._status.set_failed( - "Unhandled error happened: {}".format(str(exc)), + "Unhandled error happened for `{}`: {}".format( + product_name, str(exc) + ), (_exc, _value, _tb) ) From de281f34c39e93cb1a5c0948470c17f6e92ffb79 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 21 Aug 2025 15:09:01 +0200 Subject: [PATCH 27/40] Remove unnecessary _process_item_ids --- client/ayon_core/tools/push_to_project/ui/window.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/tools/push_to_project/ui/window.py b/client/ayon_core/tools/push_to_project/ui/window.py index 4d947103be..f5ee5f247c 100644 --- a/client/ayon_core/tools/push_to_project/ui/window.py +++ b/client/ayon_core/tools/push_to_project/ui/window.py @@ -335,7 +335,6 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self._main_thread_timer = main_thread_timer self._main_thread_timer_can_stop = True self._last_submit_message = None - self._process_item_ids = [] self._variant_is_valid = None self._folder_is_valid = None From cb4df370670edfe7e6a56ed39c4d24afac134867 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 21 Aug 2025 16:15:07 +0200 Subject: [PATCH 28/40] Use property instead private variable --- .../tools/push_to_project/models/integrate.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index 054a5f1b18..dadae7e1f9 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -317,7 +317,7 @@ class ProjectPushRepreItem: if self._src_files is not None: return self._src_files, self._resource_files - repre_context = self._repre_entity["context"] + repre_context = self.repre_entity["context"] if "frame" in repre_context or "udim" in repre_context: src_files, resource_files = self._get_source_files_with_frames() else: @@ -334,7 +334,7 @@ class ProjectPushRepreItem: udim_placeholder = "__udim__" src_files = [] resource_files = [] - template = self._repre_entity["attrib"]["template"] + template = self.repre_entity["attrib"]["template"] # Remove padding from 'udim' and 'frame' formatting keys # - "{frame:0>4}" -> "{frame}" for key in ("udim", "frame"): @@ -342,7 +342,7 @@ class ProjectPushRepreItem: replacement = "{{{}}}".format(key) template = re.sub(sub_part, replacement, template) - repre_context = self._repre_entity["context"] + repre_context = self.repre_entity["context"] fill_repre_context = copy.deepcopy(repre_context) if "frame" in fill_repre_context: fill_repre_context["frame"] = frame_placeholder @@ -363,7 +363,7 @@ class ProjectPushRepreItem: .replace(udim_placeholder, "(?P[0-9]+)") ) src_basename_regex = re.compile("^{}$".format(src_basename)) - for file_info in self._repre_entity["files"]: + for file_info in self.repre_entity["files"]: filepath_template = self._clean_path(file_info["path"]) filepath = self._clean_path( filepath_template.format(root=self._roots) @@ -394,8 +394,8 @@ class ProjectPushRepreItem: def _get_source_files(self): src_files = [] resource_files = [] - template = self._repre_entity["attrib"]["template"] - repre_context = self._repre_entity["context"] + template = self.repre_entity["attrib"]["template"] + repre_context = self.repre_entity["context"] fill_repre_context = copy.deepcopy(repre_context) fill_roots = fill_repre_context["root"] for root_name in tuple(fill_roots.keys()): @@ -404,7 +404,7 @@ class ProjectPushRepreItem: fill_repre_context) repre_path = self._clean_path(repre_path) src_dirpath = os.path.dirname(repre_path) - for file_info in self._repre_entity["files"]: + for file_info in self.repre_entity["files"]: filepath_template = self._clean_path(file_info["path"]) filepath = self._clean_path( filepath_template.format(root=self._roots)) From 4229950f361718ffc81748290f96037377e98c75 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 21 Aug 2025 17:16:30 +0200 Subject: [PATCH 29/40] Do not overwrite source entity context --- client/ayon_core/tools/push_to_project/models/integrate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index dadae7e1f9..f9de351632 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -1030,7 +1030,7 @@ class ProjectPushItemProcess: if not repre_context: repre_context = self._update_repre_context( - repre_entity, + copy.deepcopy(repre_entity), formatting_data ) From 2bd5418caeb7a753f155b588891b4a9f16d9c883 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 21 Aug 2025 17:56:13 +0200 Subject: [PATCH 30/40] Simplified querying status of ProjectPushItemProcess Makes error logging more stable, limits hard fails in debugger. --- .../tools/push_to_project/control.py | 7 +++++-- .../tools/push_to_project/models/integrate.py | 19 ++++--------------- .../tools/push_to_project/ui/window.py | 6 ++---- 3 files changed, 11 insertions(+), 21 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/control.py b/client/ayon_core/tools/push_to_project/control.py index 58d06dd19d..466dfcc994 100644 --- a/client/ayon_core/tools/push_to_project/control.py +++ b/client/ayon_core/tools/push_to_project/control.py @@ -1,4 +1,5 @@ import threading +from typing import Dict import ayon_api @@ -13,6 +14,7 @@ from .models import ( UserPublishValuesModel, IntegrateModel, ) +from .models.integrate import ProjectPushItemProcess class PushToContextController: @@ -171,8 +173,9 @@ class PushToContextController: def set_selected_task(self, task_id, task_name): self._selection_model.set_selected_task(task_id, task_name) - def get_process_item_status(self, item_id): - return self._integrate_model.get_item_status(item_id) + def get_process_items(self) -> Dict[str, ProjectPushItemProcess]: + """Returns dict of all ProjectPushItemProcess items """ + return self._integrate_model.get_items() # Processing methods def submit(self, wait=True): diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index f9de351632..ed5c5b31ab 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -5,7 +5,7 @@ import itertools import sys import traceback import uuid -from typing import Optional +from typing import Optional, Dict import ayon_api from ayon_api.utils import create_entity_id @@ -1281,17 +1281,6 @@ class IntegrateModel: return item.integrate() - def get_item_status(self, item_id): - """Status of an item. - - Args: - item_id (str): Item id for which status should be returned. - - Returns: - dict[str, Any]: Status data. - """ - - item = self._process_items.get(item_id) - if item is not None: - return item.get_status_data() - return None + def get_items(self) -> Dict[str, ProjectPushItemProcess]: + """Returns dict of all ProjectPushItemProcess items """ + return self._process_items diff --git a/client/ayon_core/tools/push_to_project/ui/window.py b/client/ayon_core/tools/push_to_project/ui/window.py index f5ee5f247c..d01da4cb3f 100644 --- a/client/ayon_core/tools/push_to_project/ui/window.py +++ b/client/ayon_core/tools/push_to_project/ui/window.py @@ -528,10 +528,8 @@ class PushToContextSelectWindow(QtWidgets.QWidget): failed_pushes = [] fail_tracebacks = [] - for process_item_id in self._controller._process_item_ids: - process_status = self._controller.get_process_item_status( - process_item_id - ) + for process_item in self._controller.get_process_items().values(): + process_status = process_item.get_status_data() if process_status["failed"]: failed_pushes.append(process_status) # push_failed = process_status["failed"] From dc987ed64f5cdbc0edb1d0985661905cbfdb6bb1 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 21 Aug 2025 18:00:42 +0200 Subject: [PATCH 31/40] Ruff --- client/ayon_core/tools/push_to_project/models/integrate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index ed5c5b31ab..ef49838152 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -847,8 +847,8 @@ class ProjectPushItemProcess: except TaskNotSetError: self._status.set_failed( "Target product name template requires task name. To " - "continue you have to select target task or change settings " - " ayon+settings://core/tools/creator/product_name_profiles" + "continue you have to select target task or change settings " # noqa: E501 + " ayon+settings://core/tools/creator/product_name_profiles" # noqa: E501 f"?project={self._item.dst_project_name}." ) raise PushToProjectError(self._status.fail_reason) From 651fc3f068a5ce0dfc9a976c47ae36a2120286fa Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 21 Aug 2025 18:07:19 +0200 Subject: [PATCH 32/40] Add validation for only single folder products selection --- client/ayon_core/plugins/load/push_to_project.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/ayon_core/plugins/load/push_to_project.py b/client/ayon_core/plugins/load/push_to_project.py index d5dd8960a3..33f9a68b23 100644 --- a/client/ayon_core/plugins/load/push_to_project.py +++ b/client/ayon_core/plugins/load/push_to_project.py @@ -28,6 +28,10 @@ class PushToProject(load.ProductLoaderPlugin): if not filtered_contexts: raise LoadError("Nothing to push for your selection") + folder_ids = [context["folder"]["id"] for context in filtered_contexts] + if len(folder_ids) > 1: + raise LoadError("Please select products from single folder") + push_tool_script_path = os.path.join( AYON_CORE_ROOT, "tools", From 16828011d22c633f0a2c9473c1f9ca2029397829 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 22 Aug 2025 13:40:26 +0200 Subject: [PATCH 33/40] Fix wrong check on folders --- client/ayon_core/plugins/load/push_to_project.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/load/push_to_project.py b/client/ayon_core/plugins/load/push_to_project.py index 33f9a68b23..0b218d6ea1 100644 --- a/client/ayon_core/plugins/load/push_to_project.py +++ b/client/ayon_core/plugins/load/push_to_project.py @@ -28,7 +28,10 @@ class PushToProject(load.ProductLoaderPlugin): if not filtered_contexts: raise LoadError("Nothing to push for your selection") - folder_ids = [context["folder"]["id"] for context in filtered_contexts] + folder_ids = set( + context["folder"]["id"] + for context in filtered_contexts + ) if len(folder_ids) > 1: raise LoadError("Please select products from single folder") From 22d6819a322ed126ccde1a3f410bd080ba47e718 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 28 Aug 2025 10:51:39 +0200 Subject: [PATCH 34/40] Updated docstring --- client/ayon_core/tools/push_to_project/control.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/ayon_core/tools/push_to_project/control.py b/client/ayon_core/tools/push_to_project/control.py index 466dfcc994..ad7cc58c5c 100644 --- a/client/ayon_core/tools/push_to_project/control.py +++ b/client/ayon_core/tools/push_to_project/control.py @@ -57,6 +57,9 @@ class PushToContextController: def set_source(self, project_name, version_ids): """Set source project and version. + There is currently assumption that tool is working on products of same + folder. + Args: project_name (Union[str, None]): Source project name. version_ids (Optional[list[str]]): Version ids. From 763c650a9f133623a6e4d1d768ecad9bb99896b3 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 28 Aug 2025 11:03:13 +0200 Subject: [PATCH 35/40] Cache product entities --- client/ayon_core/tools/push_to_project/control.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/control.py b/client/ayon_core/tools/push_to_project/control.py index ad7cc58c5c..2f712337a4 100644 --- a/client/ayon_core/tools/push_to_project/control.py +++ b/client/ayon_core/tools/push_to_project/control.py @@ -33,6 +33,7 @@ class PushToContextController: self._src_folder_entity = None self._src_folder_task_entities = {} self._src_version_entities = [] + self._src_product_entities = {} self._src_label = None self._submission_enabled = False @@ -110,6 +111,10 @@ class PushToContextController: self._src_folder_entity = folder_entity self._src_folder_task_entities = task_entities self._src_version_entities = version_entities + self._src_product_entities = { + product["id"]: product + for product in product_entities + } if folder_entity: self._user_values.set_new_folder_name(folder_entity["name"]) variant = self._get_src_variant() @@ -233,8 +238,7 @@ class PushToContextController: folder_path = folder_entity["path"] src_labels = [] for version_entity in self._src_version_entities: - product_entity = ayon_api.get_product_by_id( - self._src_project_name, + product_entity = self._src_product_entities.get( version_entity["productId"] ) src_labels.append( @@ -289,8 +293,7 @@ class PushToContextController: task_name, task_type = self._get_task_info_from_repre_entities( task_entities, repre_entities ) - product_entity = ayon_api.get_product_by_id( - project_name, + product_entity = self._src_product_entities.get( version_entity["productId"] ) From f6efb6c80dcf86bd2c61f3d3137404099d49b6a1 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 29 Aug 2025 14:08:16 +0200 Subject: [PATCH 36/40] Expose check for original names requirement --- client/ayon_core/tools/push_to_project/control.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/client/ayon_core/tools/push_to_project/control.py b/client/ayon_core/tools/push_to_project/control.py index 2f712337a4..b4e0d56dfd 100644 --- a/client/ayon_core/tools/push_to_project/control.py +++ b/client/ayon_core/tools/push_to_project/control.py @@ -158,6 +158,14 @@ class PushToContextController: def get_user_values(self): return self._user_values.get_data() + def original_names_required(self): + """Checks if original product names must be used. + + Currently simple check if multiple versions, but if multiple products + with different product_type were used, it wouldn't be necessary. + """ + return len(self._src_version_entities) > 1 + def set_user_value_folder_name(self, folder_name): self._user_values.set_new_folder_name(folder_name) self._invalidate() From 47be2d41c5cd06bd6c2d0e077a1334d08559f165 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 29 Aug 2025 14:09:44 +0200 Subject: [PATCH 37/40] Exposed _use_original_names_checkbox --- client/ayon_core/tools/push_to_project/ui/window.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/tools/push_to_project/ui/window.py b/client/ayon_core/tools/push_to_project/ui/window.py index d01da4cb3f..99b4d6ecb3 100644 --- a/client/ayon_core/tools/push_to_project/ui/window.py +++ b/client/ayon_core/tools/push_to_project/ui/window.py @@ -307,6 +307,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self._new_folder_checkbox = new_folder_checkbox self._folder_name_input = folder_name_input self._comment_input = comment_input + self._use_original_names_checkbox = original_names_checkbox self._publish_btn = publish_btn From 779fa33be21f65966d708421a9b41f5c9cb77a1c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 29 Aug 2025 14:10:44 +0200 Subject: [PATCH 38/40] Added function to decide state of _use_original_names_checkbox --- .../ayon_core/tools/push_to_project/ui/window.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/ui/window.py b/client/ayon_core/tools/push_to_project/ui/window.py index 99b4d6ecb3..3867e98b3b 100644 --- a/client/ayon_core/tools/push_to_project/ui/window.py +++ b/client/ayon_core/tools/push_to_project/ui/window.py @@ -368,6 +368,8 @@ class PushToContextSelectWindow(QtWidgets.QWidget): user_values = self._controller.get_user_values() new_folder_name = user_values["new_folder_name"] variant = user_values["variant"] + self._invalidate_use_original_names( + self._use_original_names_checkbox.isChecked()) self._folder_name_input.setText(new_folder_name or "") self._variant_input.setText(variant or "") self._invalidate_variant(user_values["is_variant_valid"]) @@ -420,9 +422,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget): def _on_original_names_change(self, state: int) -> None: use_original_name = bool(state) - self._variant_input.setEnabled(not use_original_name) - self._controller._use_original_name = use_original_name - self.refresh() + self._invalidate_use_original_names(use_original_name) def _on_user_input_timer(self): folder_name_enabled = self._new_folder_name_enabled @@ -499,6 +499,16 @@ class PushToContextSelectWindow(QtWidgets.QWidget): state = "valid" if is_valid else "invalid" set_style_property(self._variant_input, "state", state) + def _invalidate_use_original_names(self, use_original_names): + variant_used = True + if self._controller.original_names_required(): + variant_used = False + use_original_names = True + + self._controller._use_original_name = use_original_names + self._use_original_names_checkbox.setChecked(use_original_names) + self._variant_input.setEnabled(variant_used) + def _on_submission_change(self, event): self._publish_btn.setEnabled(event["enabled"]) From f4f94e75e8c90b2dc72562d491b507f102080528 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 29 Aug 2025 14:21:32 +0200 Subject: [PATCH 39/40] Simplified variant invalidation --- .../tools/push_to_project/ui/window.py | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/ui/window.py b/client/ayon_core/tools/push_to_project/ui/window.py index 3867e98b3b..ed38f24469 100644 --- a/client/ayon_core/tools/push_to_project/ui/window.py +++ b/client/ayon_core/tools/push_to_project/ui/window.py @@ -368,11 +368,11 @@ class PushToContextSelectWindow(QtWidgets.QWidget): user_values = self._controller.get_user_values() new_folder_name = user_values["new_folder_name"] variant = user_values["variant"] - self._invalidate_use_original_names( - self._use_original_names_checkbox.isChecked()) self._folder_name_input.setText(new_folder_name or "") self._variant_input.setText(variant or "") self._invalidate_variant(user_values["is_variant_valid"]) + self._invalidate_use_original_names( + self._use_original_names_checkbox.isChecked()) self._invalidate_new_folder_name( new_folder_name, user_values["is_new_folder_name_valid"] ) @@ -486,28 +486,27 @@ class PushToContextSelectWindow(QtWidgets.QWidget): state = "" if folder_name is not None: state = "valid" if is_valid else "invalid" - set_style_property( - self._folder_name_input, "state", state - ) + set_style_property(self._folder_name_input, "state", state) def _invalidate_variant(self, is_valid): - if self._controller._use_original_name: - is_valid = True - if self._variant_is_valid is is_valid: - return self._variant_is_valid = is_valid state = "valid" if is_valid else "invalid" set_style_property(self._variant_input, "state", state) def _invalidate_use_original_names(self, use_original_names): - variant_used = True + """Checks if original names must be used. + + Invalidates Variant if necessary + """ if self._controller.original_names_required(): - variant_used = False use_original_names = True + if use_original_names: + self._variant_input.setEnabled(not use_original_names) + self._invalidate_variant(not use_original_names) + self._controller._use_original_name = use_original_names self._use_original_names_checkbox.setChecked(use_original_names) - self._variant_input.setEnabled(variant_used) def _on_submission_change(self, event): self._publish_btn.setEnabled(event["enabled"]) From 12618488055958ff0e04c267d02dc16b42eda42e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 29 Aug 2025 14:56:25 +0200 Subject: [PATCH 40/40] Fix resetting invalid variant --- client/ayon_core/tools/push_to_project/ui/window.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/ui/window.py b/client/ayon_core/tools/push_to_project/ui/window.py index ed38f24469..f382ccce64 100644 --- a/client/ayon_core/tools/push_to_project/ui/window.py +++ b/client/ayon_core/tools/push_to_project/ui/window.py @@ -501,9 +501,8 @@ class PushToContextSelectWindow(QtWidgets.QWidget): if self._controller.original_names_required(): use_original_names = True - if use_original_names: - self._variant_input.setEnabled(not use_original_names) - self._invalidate_variant(not use_original_names) + self._variant_input.setEnabled(not use_original_names) + self._invalidate_variant(not use_original_names) self._controller._use_original_name = use_original_names self._use_original_names_checkbox.setChecked(use_original_names)