From 72bde0349b0112f558d3b742128d864de13d055f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 8 Jul 2025 17:04:35 +0200 Subject: [PATCH 01/79] Allow to push to other projects not only Library --- .../tools/push_to_project/control.py | 10 +++++++ .../tools/push_to_project/ui/window.py | 26 ++++++++++++++++++- 2 files changed, 35 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 fb080d158b..f24d11d0b7 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._library_only = True + # Events system def emit_event(self, topic, data=None, source=None): """Use implemented event system to trigger event.""" @@ -128,6 +130,14 @@ class PushToContextController: self._src_label = self._prepare_source_label() return self._src_label + def get_library_only(self): + """Returns state of library filter""" + return self._library_only + + def set_library_only(self, state: bool): + """Change state of library filter""" + self._library_only = state + def get_project_items(self, sender=None): return self._projects_model.get_project_items(sender) 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 a69c512fcd..566a0fc605 100644 --- a/client/ayon_core/tools/push_to_project/ui/window.py +++ b/client/ayon_core/tools/push_to_project/ui/window.py @@ -85,6 +85,14 @@ class PushToContextSelectWindow(QtWidgets.QWidget): header_widget = QtWidgets.QWidget(main_context_widget) + library_only = self._controller.get_library_only() + library_only_label = QtWidgets.QLabel( + "Show only libraries", + header_widget + ) + library_only_checkbox = NiceCheckbox( + library_only, parent=header_widget) + header_label = QtWidgets.QLabel( controller.get_source_label(), header_widget @@ -93,6 +101,14 @@ class PushToContextSelectWindow(QtWidgets.QWidget): header_layout = QtWidgets.QHBoxLayout(header_widget) header_layout.setContentsMargins(0, 0, 0, 0) header_layout.addWidget(header_label) + header_layout.addStretch() + + library_only_layout = QtWidgets.QHBoxLayout() + library_only_layout.addWidget(library_only_label) + library_only_layout.addWidget(library_only_checkbox) + library_only_layout.setSpacing(5) # or whatever spacing you prefer + + header_layout.addLayout(library_only_layout) main_splitter = QtWidgets.QSplitter( QtCore.Qt.Horizontal, main_context_widget @@ -102,7 +118,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget): projects_combobox = ProjectsCombobox(controller, context_widget) projects_combobox.set_select_item_visible(True) - projects_combobox.set_standard_filter_enabled(True) + projects_combobox.set_standard_filter_enabled(library_only) context_splitter = QtWidgets.QSplitter( QtCore.Qt.Vertical, context_widget @@ -240,6 +256,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget): folder_name_input.textChanged.connect(self._on_new_folder_change) 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) publish_btn.clicked.connect(self._on_select_click) cancel_btn.clicked.connect(self._on_close_click) @@ -394,6 +411,13 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self._comment_input_text = text self._user_input_changed_timer.start() + def _on_library_only_change(self, state: int) -> None: + """Change toggle state, reset filter, recalculate dropdown""" + state = bool(state) + self._controller.set_library_only(state) + self._projects_combobox.set_standard_filter_enabled(state) + self._projects_combobox.refresh() + def _on_user_input_timer(self): folder_name_enabled = self._new_folder_name_enabled folder_name = self._new_folder_name_input_text From e880b2983896cb79493266c61f08089e16d109a4 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 8 Jul 2025 17:06:11 +0200 Subject: [PATCH 02/79] Changed label of action Push to --- client/ayon_core/plugins/load/push_to_library.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/load/push_to_library.py b/client/ayon_core/plugins/load/push_to_library.py index 981028d734..825192c15e 100644 --- a/client/ayon_core/plugins/load/push_to_library.py +++ b/client/ayon_core/plugins/load/push_to_library.py @@ -14,7 +14,7 @@ class PushToLibraryProject(load.ProductLoaderPlugin): representations = {"*"} product_types = {"*"} - label = "Push to Library project" + label = "Push to (Library) project" order = 35 icon = "send" color = "#d8d8d8" From c35c86440bedd9d15475cd7db9d9685965c1777c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 10 Jul 2025 11:39:01 +0200 Subject: [PATCH 03/79] Used different layout Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/tools/push_to_project/ui/window.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 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 566a0fc605..49093b8a00 100644 --- a/client/ayon_core/tools/push_to_project/ui/window.py +++ b/client/ayon_core/tools/push_to_project/ui/window.py @@ -101,14 +101,9 @@ class PushToContextSelectWindow(QtWidgets.QWidget): header_layout = QtWidgets.QHBoxLayout(header_widget) header_layout.setContentsMargins(0, 0, 0, 0) header_layout.addWidget(header_label) - header_layout.addStretch() - - library_only_layout = QtWidgets.QHBoxLayout() - library_only_layout.addWidget(library_only_label) - library_only_layout.addWidget(library_only_checkbox) - library_only_layout.setSpacing(5) # or whatever spacing you prefer - - header_layout.addLayout(library_only_layout) + header_layout.addStretch(1) + header_layout.addWidget(library_only_label, 0) + header_layout.addWidget(library_only_checkbox, 0) main_splitter = QtWidgets.QSplitter( QtCore.Qt.Horizontal, main_context_widget From 63da40c2025739bdfc864ae38e5031fe20dcc0a9 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 11 Jul 2025 13:59:34 +0200 Subject: [PATCH 04/79] Added thumbnail copy from source to target --- .../tools/push_to_project/models/integrate.py | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) 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 6bd4279219..fd20a7faba 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -3,6 +3,7 @@ import re import copy import itertools import sys +import tempfile import traceback import uuid @@ -484,6 +485,7 @@ class ProjectPushItemProcess: self._make_sure_version_exists() self._log_info("Prerequirements were prepared") self._integrate_representations() + self._copy_version_thumbnail() self._log_info("Integration finished") except PushToProjectError as exc: @@ -1145,8 +1147,39 @@ class ProjectPushItemProcess: "representation", repre_entity["id"], {"active": False} + + def _copy_version_thumbnail(self): + version_thumbnail = ayon_api.get_version_thumbnail( + self._item.src_project_name, self._src_version_entity["id"]) + if not version_thumbnail or not version_thumbnail.id: + return + + temp_file_name = None + try: + with tempfile.NamedTemporaryFile(mode='w+b', delete=False) as fp: + fp.write(version_thumbnail.content) + temp_file_name = fp.name + + new_thumbnail_id = ayon_api.create_thumbnail( + self._item.dst_project_name, + temp_file_name ) + task_id = None + if self._task_info: + task_id = self._task_info["id"] + + self._operations.update_version( + project_name=self._item.dst_project_name, + version_id=self._version_entity["id"], + task_id=task_id, + thumbnail_id=new_thumbnail_id + ) + self._operations.commit() + finally: + if temp_file_name and os.path.exists(temp_file_name): + os.remove(temp_file_name) + class IntegrateModel: def __init__(self, controller): From 7161de78fabaddd55d5d9e1c1d6f01e167da7910 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 14 Jul 2025 16:33:44 +0200 Subject: [PATCH 05/79] Fix typo --- client/ayon_core/tools/push_to_project/models/integrate.py | 1 + 1 file changed, 1 insertion(+) 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 fd20a7faba..341858148b 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -1147,6 +1147,7 @@ class ProjectPushItemProcess: "representation", repre_entity["id"], {"active": False} + ) def _copy_version_thumbnail(self): version_thumbnail = ayon_api.get_version_thumbnail( From bcea66c9314eb9f2796cefeb6176c2f9fc485fdc Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 17 Jul 2025 14:31:23 +0200 Subject: [PATCH 06/79] Label update Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/plugins/load/push_to_library.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/load/push_to_library.py b/client/ayon_core/plugins/load/push_to_library.py index 825192c15e..22c10bbad7 100644 --- a/client/ayon_core/plugins/load/push_to_library.py +++ b/client/ayon_core/plugins/load/push_to_library.py @@ -14,7 +14,7 @@ class PushToLibraryProject(load.ProductLoaderPlugin): representations = {"*"} product_types = {"*"} - label = "Push to (Library) project" + label = "Push to project" order = 35 icon = "send" color = "#d8d8d8" From 1c4f466181a892ecb954f523de061e791d435e7e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 17 Jul 2025 14:31:34 +0200 Subject: [PATCH 07/79] Formatting change Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/tools/push_to_project/models/integrate.py | 1 + 1 file changed, 1 insertion(+) 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 341858148b..20fa5c98e5 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -1148,6 +1148,7 @@ class ProjectPushItemProcess: repre_entity["id"], {"active": False} ) + ) def _copy_version_thumbnail(self): version_thumbnail = ayon_api.get_version_thumbnail( From fd26c2039495b3f83bf8c3c08de2edc5b449c257 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 28 Jul 2025 15:00:44 +0200 Subject: [PATCH 08/79] Fix typo --- client/ayon_core/tools/push_to_project/models/integrate.py | 1 - 1 file changed, 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 20fa5c98e5..341858148b 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -1148,7 +1148,6 @@ class ProjectPushItemProcess: repre_entity["id"], {"active": False} ) - ) def _copy_version_thumbnail(self): version_thumbnail = ayon_api.get_version_thumbnail( From bcf87dec1060d1dfef2e8d4249f4ebee698829ba Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 28 Jul 2025 15:01:12 +0200 Subject: [PATCH 09/79] Removed unnecessary _library_only --- client/ayon_core/tools/push_to_project/control.py | 8 -------- client/ayon_core/tools/push_to_project/ui/window.py | 6 ++---- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/control.py b/client/ayon_core/tools/push_to_project/control.py index f24d11d0b7..eb985a3f8c 100644 --- a/client/ayon_core/tools/push_to_project/control.py +++ b/client/ayon_core/tools/push_to_project/control.py @@ -130,14 +130,6 @@ class PushToContextController: self._src_label = self._prepare_source_label() return self._src_label - def get_library_only(self): - """Returns state of library filter""" - return self._library_only - - def set_library_only(self, state: bool): - """Change state of library filter""" - self._library_only = state - def get_project_items(self, sender=None): return self._projects_model.get_project_items(sender) 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 49093b8a00..6b0363adee 100644 --- a/client/ayon_core/tools/push_to_project/ui/window.py +++ b/client/ayon_core/tools/push_to_project/ui/window.py @@ -85,13 +85,12 @@ class PushToContextSelectWindow(QtWidgets.QWidget): header_widget = QtWidgets.QWidget(main_context_widget) - library_only = self._controller.get_library_only() library_only_label = QtWidgets.QLabel( "Show only libraries", header_widget ) library_only_checkbox = NiceCheckbox( - library_only, parent=header_widget) + True, parent=header_widget) header_label = QtWidgets.QLabel( controller.get_source_label(), @@ -113,7 +112,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget): projects_combobox = ProjectsCombobox(controller, context_widget) projects_combobox.set_select_item_visible(True) - projects_combobox.set_standard_filter_enabled(library_only) + projects_combobox.set_standard_filter_enabled(True) context_splitter = QtWidgets.QSplitter( QtCore.Qt.Vertical, context_widget @@ -409,7 +408,6 @@ class PushToContextSelectWindow(QtWidgets.QWidget): def _on_library_only_change(self, state: int) -> None: """Change toggle state, reset filter, recalculate dropdown""" state = bool(state) - self._controller.set_library_only(state) self._projects_combobox.set_standard_filter_enabled(state) self._projects_combobox.refresh() From 2757c6efbb7e68c6c0ff1ac43e6d4b0bef2c2971 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Tue, 12 Aug 2025 18:42:49 +0200 Subject: [PATCH 10/79] :sparkles: very raw WIP version --- .../plugins/load/create_hero_version.py | 415 ++++++++++++++++++ 1 file changed, 415 insertions(+) create mode 100644 client/ayon_core/plugins/load/create_hero_version.py diff --git a/client/ayon_core/plugins/load/create_hero_version.py b/client/ayon_core/plugins/load/create_hero_version.py new file mode 100644 index 0000000000..7e1a0d8a3d --- /dev/null +++ b/client/ayon_core/plugins/load/create_hero_version.py @@ -0,0 +1,415 @@ + +import os +import copy +import shutil +import errno +import itertools +from concurrent.futures import ThreadPoolExecutor + +from speedcopy import copyfile +import clique +import ayon_api +from ayon_api.operations import OperationsSession, new_version_entity +from ayon_api.utils import create_entity_id +from qtpy import QtWidgets, QtCore +from ayon_core import style +from ayon_core.pipeline import load, Anatomy +from ayon_core.lib import create_hard_link, source_hash +from ayon_core.lib.file_transaction import wait_for_future_errors +from ayon_core.pipeline.publish import get_publish_template_name +from ayon_core.pipeline.template_data import get_template_data + + +def prepare_changes(old_entity, new_entity): + changes = {} + for key in set(new_entity.keys()): + if key == "attrib": + continue + if key in new_entity and new_entity[key] != old_entity.get(key): + changes[key] = new_entity[key] + attrib_changes = {} + if "attrib" in new_entity: + for key, value in new_entity["attrib"].items(): + if value != old_entity["attrib"].get(key): + attrib_changes[key] = value + if attrib_changes: + changes["attrib"] = attrib_changes + return changes + + +class CreateHeroVersion(load.ProductLoaderPlugin): + """Create hero version from selected context.""" + + is_multiple_contexts_compatible = False + representations = {"*"} + product_types = {"*"} + label = "Create Hero Version" + order = 36 + icon = "star" + color = "#ffd700" + + ignored_representation_names = [] + db_representation_context_keys = [ + "project", "folder", "asset", "hierarchy", "task", "product", + "subset", "family", "representation", "username", "user", "output" + ] + use_hardlinks = False + + def message(self, text): + msgBox = QtWidgets.QMessageBox() + msgBox.setText(text) + msgBox.setStyleSheet(style.load_stylesheet()) + msgBox.setWindowFlags( + msgBox.windowFlags() | QtCore.Qt.FramelessWindowHint + ) + msgBox.exec_() + + + def load(self, context, name=None, namespace=None, options=None) -> None: + """Load hero version from context (dict as in context.py).""" + success = True + errors = [] + + # Extract project, product, version, folder from context + project = context.get("project") + product = context.get("product") + version = context.get("version") + folder = context.get("folder") + task_entity = ayon_api.get_task_by_id( + task_id=version.get("taskId"), project_name=project["name"]) + + anatomy = Anatomy(project["name"]) + + version_id = version["id"] + project_name = project["name"] + repres = list( + ayon_api.get_representations(project_name, version_ids={version_id})) + anatomy_data = get_template_data( + project_entity=project, + folder_entity=folder, + task_entity=task_entity, + ) + anatomy_data["product"] = { + "name": product["name"], + "type": product["productType"], + } + published_representations = {} + for repre in repres: + repre_anatomy = anatomy_data + repre_anatomy["ext"] = repre.get("ext", "") + published_representations[repre["id"]] = { + "representation": repre, + "published_files": [f["path"] for f in repre.get("files", [])], + "anatomy_data": repre_anatomy + } + + instance_data = { + "productName": product["name"], + "productType": product["productType"], + "anatomyData": anatomy_data, + "publishDir": "", # TODO: Set to actual publish directory + "published_representations": published_representations, + "versionEntity": version, + } + + try: + self.create_hero_version(instance_data, anatomy, context) + except Exception as exc: + success = False + errors.append(str(exc)) + if success: + self.message("Hero version created successfully.") + else: + self.message( + f"Failed to create hero version:\n{chr(10).join(errors)}") + + def create_hero_version(self, instance_data, anatomy, context): + """Create hero version from instance data.""" + published_repres = instance_data.get("published_representations") + if not published_repres: + raise RuntimeError("No published representations found.") + + project_name = anatomy.project_name + template_key = get_publish_template_name( + project_name, + context.get("hostName"), + instance_data.get("productType"), + instance_data.get("anatomyData", {}).get("task", {}).get("name"), + instance_data.get("anatomyData", {}).get("task", {}).get("type"), + project_settings=context.get("project_settings", {}), + hero=True, + logger=None + ) + hero_template = anatomy.get_template_item("hero", template_key, "path", default=None) + if hero_template is None: + raise RuntimeError(f"Project anatomy does not have hero template key: {template_key}") + + print(f"Hero template: {hero_template.template}") + + hero_publish_dir = self.get_publish_dir(instance_data, anatomy, template_key) + + print(f"Hero publish dir: {hero_publish_dir}") + + src_version_entity = instance_data.get("versionEntity") + filtered_repre_ids = [] + for repre_id, repre_info in published_repres.items(): + repre = repre_info["representation"] + if repre["name"].lower() in self.ignored_representation_names: + filtered_repre_ids.append(repre_id) + for repre_id in filtered_repre_ids: + published_repres.pop(repre_id, None) + if not published_repres: + raise RuntimeError("All published representations were filtered by name.") + + if src_version_entity is None: + src_version_entity = self.version_from_representations(project_name, published_repres) + if not src_version_entity: + raise RuntimeError("Can't find origin version in database.") + if src_version_entity["version"] == 0: + raise RuntimeError("Version 0 cannot have hero version.") + + all_copied_files = [] + transfers = instance_data.get("transfers", list()) + for _src, dst in transfers: + dst = os.path.normpath(dst) + if dst not in all_copied_files: + all_copied_files.append(dst) + hardlinks = instance_data.get("hardlinks", list()) + for _src, dst in hardlinks: + dst = os.path.normpath(dst) + if dst not in all_copied_files: + all_copied_files.append(dst) + + all_repre_file_paths = [] + for repre_info in published_repres.values(): + published_files = repre_info.get("published_files") or [] + for file_path in published_files: + file_path = os.path.normpath(file_path) + if file_path not in all_repre_file_paths: + all_repre_file_paths.append(file_path) + + instance_publish_dir = os.path.normpath(instance_data["publishDir"]) + other_file_paths_mapping = [] + for file_path in all_copied_files: + if not file_path.startswith(instance_publish_dir): + continue + if file_path in all_repre_file_paths: + continue + dst_filepath = file_path.replace(instance_publish_dir, hero_publish_dir) + other_file_paths_mapping.append((file_path, dst_filepath)) + + old_version, old_repres = self.current_hero_ents(project_name, src_version_entity) + inactive_old_repres_by_name = {} + old_repres_by_name = {} + for repre in old_repres: + low_name = repre["name"].lower() + if repre["active"]: + old_repres_by_name[low_name] = repre + else: + inactive_old_repres_by_name[low_name] = repre + + op_session = OperationsSession() + entity_id = old_version["id"] if old_version else None + new_hero_version = new_version_entity( + -src_version_entity["version"], + src_version_entity["productId"], + task_id=src_version_entity.get("taskId"), + data=copy.deepcopy(src_version_entity["data"]), + attribs=copy.deepcopy(src_version_entity["attrib"]), + entity_id=entity_id, + ) + if old_version: + update_data = prepare_changes(old_version, new_hero_version) + op_session.update_entity(project_name, "version", old_version["id"], update_data) + else: + op_session.create_entity(project_name, "version", new_hero_version) + + # Store hero entity to instance_data + instance_data["heroVersionEntity"] = new_hero_version + + old_repres_to_replace = {} + old_repres_to_delete = {} + for repre_info in published_repres.values(): + repre = repre_info["representation"] + repre_name_low = repre["name"].lower() + if repre_name_low in old_repres_by_name: + old_repres_to_replace[repre_name_low] = old_repres_by_name.pop(repre_name_low) + if old_repres_by_name: + old_repres_to_delete = old_repres_by_name + + backup_hero_publish_dir = None + if os.path.exists(hero_publish_dir): + backup_hero_publish_dir = hero_publish_dir + ".BACKUP" + max_idx = 10 + idx = 0 + _backup_hero_publish_dir = backup_hero_publish_dir + while os.path.exists(_backup_hero_publish_dir): + try: + shutil.rmtree(_backup_hero_publish_dir) + backup_hero_publish_dir = _backup_hero_publish_dir + break + except Exception: + _backup_hero_publish_dir = backup_hero_publish_dir + str(idx) + if not os.path.exists(_backup_hero_publish_dir): + backup_hero_publish_dir = _backup_hero_publish_dir + break + if idx > max_idx: + raise AssertionError(f"Backup folders are fully occupied to max index {max_idx}") + idx += 1 + try: + os.rename(hero_publish_dir, backup_hero_publish_dir) + except PermissionError: + raise AssertionError( + "Could not create hero version because it is " + "not possible to replace current hero files.") + + try: + src_to_dst_file_paths = [] + repre_integrate_data = [] + path_template_obj = anatomy.get_template_item( + "hero", template_key, "path") + for repre_info in published_repres.values(): + published_files = repre_info["published_files"] + if len(published_files) == 0: + continue + anatomy_data = copy.deepcopy(repre_info["anatomy_data"]) + anatomy_data.pop("version", None) + template_filled = path_template_obj.format_strict(anatomy_data) + repre_context = template_filled.used_values + for key in self.db_representation_context_keys: + value = anatomy_data.get(key) + if value is not None: + repre_context[key] = value + repre_entity = copy.deepcopy(repre_info["representation"]) + repre_entity.pop("id", None) + repre_entity["versionId"] = new_hero_version["id"] + repre_entity["context"] = repre_context + repre_entity["attrib"] = { + "path": str(template_filled), + "template": hero_template.template + } + dst_paths = [] + if len(published_files) == 1: + dst_paths.append(str(template_filled)) + src_to_dst_file_paths.append((published_files[0], template_filled)) + print(f"Single published file: {published_files[0]} -> {template_filled}") + else: + collections, remainders = clique.assemble(published_files) + if remainders or not collections or len(collections) > 1: + raise Exception( + "Integrity error. Files of published representation is " + "combination of frame collections and single files.") + src_col = collections[0] + frame_splitter = "_-_FRAME_SPLIT_-_" + anatomy_data["frame"] = frame_splitter + _template_filled = path_template_obj.format_strict(anatomy_data) + head, tail = _template_filled.split(frame_splitter) + padding = anatomy.templates_obj.frame_padding + dst_col = clique.Collection(head=head, padding=padding, tail=tail) + dst_col.indexes.clear() + dst_col.indexes.update(src_col.indexes) + for src_file, dst_file in zip(src_col, dst_col): + src_to_dst_file_paths.append((src_file, dst_file)) + dst_paths.append(dst_file) + print(f"Collection published file: {src_file} -> {dst_file}") + repre_integrate_data.append((repre_entity, dst_paths)) + + # Copy files + with ThreadPoolExecutor(max_workers=8) as executor: + futures = [ + executor.submit(self.copy_file, src_path, dst_path) + for src_path, dst_path in itertools.chain(src_to_dst_file_paths, other_file_paths_mapping) + ] + wait_for_future_errors(executor, futures) + + # Update/create representations + for repre_entity, dst_paths in repre_integrate_data: + repre_files = self.get_files_info(dst_paths, anatomy) + repre_entity["files"] = repre_files + repre_name_low = repre_entity["name"].lower() + if repre_name_low in old_repres_to_replace: + old_repre = old_repres_to_replace.pop(repre_name_low) + repre_entity["id"] = old_repre["id"] + update_data = prepare_changes(old_repre, repre_entity) + op_session.update_entity(project_name, "representation", old_repre["id"], update_data) + elif repre_name_low in inactive_old_repres_by_name: + inactive_repre = inactive_old_repres_by_name.pop(repre_name_low) + repre_entity["id"] = inactive_repre["id"] + update_data = prepare_changes(inactive_repre, repre_entity) + op_session.update_entity(project_name, "representation", inactive_repre["id"], update_data) + else: + op_session.create_entity(project_name, "representation", repre_entity) + + for repre in old_repres_to_delete.values(): + op_session.update_entity(project_name, "representation", repre["id"], {"active": False}) + + op_session.commit() + + if backup_hero_publish_dir is not None and os.path.exists(backup_hero_publish_dir): + shutil.rmtree(backup_hero_publish_dir) + + except Exception: + if backup_hero_publish_dir is not None and os.path.exists(backup_hero_publish_dir): + if os.path.exists(hero_publish_dir): + shutil.rmtree(hero_publish_dir) + os.rename(backup_hero_publish_dir, hero_publish_dir) + raise + + def get_files_info(self, filepaths, anatomy): + file_infos = [] + for filepath in filepaths: + file_info = self.prepare_file_info(filepath, anatomy) + file_infos.append(file_info) + return file_infos + + def prepare_file_info(self, path, anatomy): + return { + "id": create_entity_id(), + "name": os.path.basename(path), + "path": self.get_rootless_path(anatomy, path), + "size": os.path.getsize(path), + "hash": source_hash(path), + "hash_type": "op3", + } + + def get_publish_dir(self, instance_data, anatomy, template_key): + template_data = copy.deepcopy(instance_data.get("anatomyData", {})) + if "originalBasename" in instance_data: + template_data["originalBasename"] = instance_data["originalBasename"] + template_obj = anatomy.get_template_item("hero", template_key, "directory") + return os.path.normpath(template_obj.format_strict(template_data)) + + def get_rootless_path(self, anatomy, path): + success, rootless_path = anatomy.find_root_template_from_path(path) + if success: + path = rootless_path + return path + + def copy_file(self, src_path, dst_path): + dirname = os.path.dirname(dst_path) + try: + os.makedirs(dirname) + except OSError as exc: + if exc.errno != errno.EEXIST: + raise + if self.use_hardlinks: + try: + create_hard_link(src_path, dst_path) + return + except OSError as exc: + if exc.errno not in [errno.EXDEV, errno.EINVAL]: + raise + copyfile(src_path, dst_path) + + def version_from_representations(self, project_name, repres): + for repre_info in repres.values(): + version = ayon_api.get_version_by_id(project_name, repre_info["representation"]["versionId"]) + if version: + return version + + def current_hero_ents(self, project_name, version): + hero_version = ayon_api.get_hero_version_by_product_id(project_name, version["productId"]) + if not hero_version: + return (None, []) + hero_repres = list(ayon_api.get_representations(project_name, version_ids={hero_version["id"]})) + return (hero_version, hero_repres) From 65d03327b8af6e630b41f17bd629115e6fd1a83d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 13 Aug 2025 15:18:46 +0200 Subject: [PATCH 11/79] Fix typo --- 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 6b0363adee..344295f177 100644 --- a/client/ayon_core/tools/push_to_project/ui/window.py +++ b/client/ayon_core/tools/push_to_project/ui/window.py @@ -551,7 +551,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self._main_thread_timer_can_stop = False self._main_thread_timer.start() self._main_layout.setCurrentWidget(self._overlay_widget) - self._overlay_label.setText("Submittion started") + self._overlay_label.setText("Submission started") def _on_controller_submit_end(self): self._main_thread_timer_can_stop = True From 9ffaa15dfb4ec0484a5303ed1a8a04bd2805c7e9 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 15 Aug 2025 22:09:15 +0800 Subject: [PATCH 12/79] make sure the hero version can be created successfully by ensuring to copy the path into the right path --- .../plugins/load/create_hero_version.py | 141 ++++++++++++++---- 1 file changed, 110 insertions(+), 31 deletions(-) diff --git a/client/ayon_core/plugins/load/create_hero_version.py b/client/ayon_core/plugins/load/create_hero_version.py index 7e1a0d8a3d..b18e874644 100644 --- a/client/ayon_core/plugins/load/create_hero_version.py +++ b/client/ayon_core/plugins/load/create_hero_version.py @@ -14,7 +14,7 @@ from ayon_api.utils import create_entity_id from qtpy import QtWidgets, QtCore from ayon_core import style from ayon_core.pipeline import load, Anatomy -from ayon_core.lib import create_hard_link, source_hash +from ayon_core.lib import create_hard_link, source_hash, StringTemplate from ayon_core.lib.file_transaction import wait_for_future_errors from ayon_core.pipeline.publish import get_publish_template_name from ayon_core.pipeline.template_data import get_template_data @@ -83,7 +83,10 @@ class CreateHeroVersion(load.ProductLoaderPlugin): version_id = version["id"] project_name = project["name"] repres = list( - ayon_api.get_representations(project_name, version_ids={version_id})) + ayon_api.get_representations( + project_name, version_ids={version_id} + ) + ) anatomy_data = get_template_data( project_entity=project, folder_entity=folder, @@ -95,8 +98,9 @@ class CreateHeroVersion(load.ProductLoaderPlugin): } published_representations = {} for repre in repres: - repre_anatomy = anatomy_data - repre_anatomy["ext"] = repre.get("ext", "") + repre_anatomy = copy.deepcopy(anatomy_data) + if "ext" not in repre_anatomy: + repre_anatomy["ext"] = repre.get("context", {}).get("ext", "") published_representations[repre["id"]] = { "representation": repre, "published_files": [f["path"] for f in repre.get("files", [])], @@ -140,13 +144,18 @@ class CreateHeroVersion(load.ProductLoaderPlugin): hero=True, logger=None ) - hero_template = anatomy.get_template_item("hero", template_key, "path", default=None) + hero_template = anatomy.get_template_item( + "hero", template_key, "path", default=None + ) if hero_template is None: - raise RuntimeError(f"Project anatomy does not have hero template key: {template_key}") + raise RuntimeError("Project anatomy does not have hero " + f"template key: {template_key}") print(f"Hero template: {hero_template.template}") - hero_publish_dir = self.get_publish_dir(instance_data, anatomy, template_key) + hero_publish_dir = self.get_publish_dir( + instance_data, anatomy, template_key + ) print(f"Hero publish dir: {hero_publish_dir}") @@ -162,7 +171,8 @@ class CreateHeroVersion(load.ProductLoaderPlugin): raise RuntimeError("All published representations were filtered by name.") if src_version_entity is None: - src_version_entity = self.version_from_representations(project_name, published_repres) + src_version_entity = self.version_from_representations( + project_name, published_repres) if not src_version_entity: raise RuntimeError("Can't find origin version in database.") if src_version_entity["version"] == 0: @@ -195,10 +205,14 @@ class CreateHeroVersion(load.ProductLoaderPlugin): continue if file_path in all_repre_file_paths: continue - dst_filepath = file_path.replace(instance_publish_dir, hero_publish_dir) + dst_filepath = file_path.replace( + instance_publish_dir, hero_publish_dir + ) other_file_paths_mapping.append((file_path, dst_filepath)) - old_version, old_repres = self.current_hero_ents(project_name, src_version_entity) + old_version, old_repres = self.current_hero_ents( + project_name, src_version_entity + ) inactive_old_repres_by_name = {} old_repres_by_name = {} for repre in old_repres: @@ -220,7 +234,9 @@ class CreateHeroVersion(load.ProductLoaderPlugin): ) if old_version: update_data = prepare_changes(old_version, new_hero_version) - op_session.update_entity(project_name, "version", old_version["id"], update_data) + op_session.update_entity( + project_name, "version", old_version["id"], update_data + ) else: op_session.create_entity(project_name, "version", new_hero_version) @@ -233,7 +249,9 @@ class CreateHeroVersion(load.ProductLoaderPlugin): repre = repre_info["representation"] repre_name_low = repre["name"].lower() if repre_name_low in old_repres_by_name: - old_repres_to_replace[repre_name_low] = old_repres_by_name.pop(repre_name_low) + old_repres_to_replace[repre_name_low] = ( + old_repres_by_name.pop(repre_name_low) + ) if old_repres_by_name: old_repres_to_delete = old_repres_by_name @@ -254,7 +272,9 @@ class CreateHeroVersion(load.ProductLoaderPlugin): backup_hero_publish_dir = _backup_hero_publish_dir break if idx > max_idx: - raise AssertionError(f"Backup folders are fully occupied to max index {max_idx}") + raise AssertionError( + f"Backup folders are fully occupied to max index {max_idx}" + ) idx += 1 try: os.rename(hero_publish_dir, backup_hero_publish_dir) @@ -268,6 +288,7 @@ class CreateHeroVersion(load.ProductLoaderPlugin): repre_integrate_data = [] path_template_obj = anatomy.get_template_item( "hero", template_key, "path") + anatomy_root = {"root": anatomy.roots} for repre_info in published_repres.values(): published_files = repre_info["published_files"] if len(published_files) == 0: @@ -289,10 +310,21 @@ class CreateHeroVersion(load.ProductLoaderPlugin): "template": hero_template.template } dst_paths = [] + if len(published_files) == 1: dst_paths.append(str(template_filled)) - src_to_dst_file_paths.append((published_files[0], template_filled)) - print(f"Single published file: {published_files[0]} -> {template_filled}") + mapped_published_file = StringTemplate( + published_files[0]).format_strict( + anatomy_root + ) + src_to_dst_file_paths.append( + (mapped_published_file, template_filled) + ) + print( + f"Single published file: {mapped_published_file} -> " + f"{template_filled}" + ) + # src_to_dst_file_paths being wrong else: collections, remainders = clique.assemble(published_files) if remainders or not collections or len(collections) > 1: @@ -302,23 +334,34 @@ class CreateHeroVersion(load.ProductLoaderPlugin): src_col = collections[0] frame_splitter = "_-_FRAME_SPLIT_-_" anatomy_data["frame"] = frame_splitter - _template_filled = path_template_obj.format_strict(anatomy_data) + _template_filled = path_template_obj.format_strict( + anatomy_data + ) head, tail = _template_filled.split(frame_splitter) padding = anatomy.templates_obj.frame_padding - dst_col = clique.Collection(head=head, padding=padding, tail=tail) + dst_col = clique.Collection( + head=head, padding=padding, tail=tail + ) dst_col.indexes.clear() dst_col.indexes.update(src_col.indexes) for src_file, dst_file in zip(src_col, dst_col): + src_file = StringTemplate(src_file).format_strict( + anatomy_root + ) src_to_dst_file_paths.append((src_file, dst_file)) dst_paths.append(dst_file) - print(f"Collection published file: {src_file} -> {dst_file}") + print( + f"Collection published file: {src_file} " + f"-> {dst_file}" + ) repre_integrate_data.append((repre_entity, dst_paths)) # Copy files with ThreadPoolExecutor(max_workers=8) as executor: futures = [ executor.submit(self.copy_file, src_path, dst_path) - for src_path, dst_path in itertools.chain(src_to_dst_file_paths, other_file_paths_mapping) + for src_path, dst_path in itertools.chain( + src_to_dst_file_paths, other_file_paths_mapping) ] wait_for_future_errors(executor, futures) @@ -331,25 +374,49 @@ class CreateHeroVersion(load.ProductLoaderPlugin): old_repre = old_repres_to_replace.pop(repre_name_low) repre_entity["id"] = old_repre["id"] update_data = prepare_changes(old_repre, repre_entity) - op_session.update_entity(project_name, "representation", old_repre["id"], update_data) + op_session.update_entity( + project_name, + "representation", + old_repre["id"], + update_data + ) elif repre_name_low in inactive_old_repres_by_name: - inactive_repre = inactive_old_repres_by_name.pop(repre_name_low) + inactive_repre = inactive_old_repres_by_name.pop( + repre_name_low + ) repre_entity["id"] = inactive_repre["id"] update_data = prepare_changes(inactive_repre, repre_entity) - op_session.update_entity(project_name, "representation", inactive_repre["id"], update_data) + op_session.update_entity( + project_name, + "representation", + inactive_repre["id"], + update_data + ) else: - op_session.create_entity(project_name, "representation", repre_entity) + op_session.create_entity( + project_name, + "representation", + repre_entity + ) for repre in old_repres_to_delete.values(): - op_session.update_entity(project_name, "representation", repre["id"], {"active": False}) + op_session.update_entity( + project_name, + "representation", + repre["id"], + {"active": False} + ) op_session.commit() - if backup_hero_publish_dir is not None and os.path.exists(backup_hero_publish_dir): + if backup_hero_publish_dir is not None and os.path.exists( + backup_hero_publish_dir + ): shutil.rmtree(backup_hero_publish_dir) except Exception: - if backup_hero_publish_dir is not None and os.path.exists(backup_hero_publish_dir): + if backup_hero_publish_dir is not None and os.path.exists( + backup_hero_publish_dir): if os.path.exists(hero_publish_dir): shutil.rmtree(hero_publish_dir) os.rename(backup_hero_publish_dir, hero_publish_dir) @@ -375,8 +442,12 @@ class CreateHeroVersion(load.ProductLoaderPlugin): def get_publish_dir(self, instance_data, anatomy, template_key): template_data = copy.deepcopy(instance_data.get("anatomyData", {})) if "originalBasename" in instance_data: - template_data["originalBasename"] = instance_data["originalBasename"] - template_obj = anatomy.get_template_item("hero", template_key, "directory") + template_data["originalBasename"] = ( + instance_data["originalBasename"] + ) + template_obj = anatomy.get_template_item( + "hero", template_key, "directory" + ) return os.path.normpath(template_obj.format_strict(template_data)) def get_rootless_path(self, anatomy, path): @@ -403,13 +474,21 @@ class CreateHeroVersion(load.ProductLoaderPlugin): def version_from_representations(self, project_name, repres): for repre_info in repres.values(): - version = ayon_api.get_version_by_id(project_name, repre_info["representation"]["versionId"]) + version = ayon_api.get_version_by_id( + project_name, repre_info["representation"]["versionId"] + ) if version: return version def current_hero_ents(self, project_name, version): - hero_version = ayon_api.get_hero_version_by_product_id(project_name, version["productId"]) + hero_version = ayon_api.get_hero_version_by_product_id( + project_name, version["productId"] + ) if not hero_version: return (None, []) - hero_repres = list(ayon_api.get_representations(project_name, version_ids={hero_version["id"]})) + hero_repres = list( + ayon_api.get_representations( + project_name, version_ids={hero_version["id"]} + ) + ) return (hero_version, hero_repres) From 7e9493736d1abbddda6b2efa37302ac03a864fe3 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 15 Aug 2025 22:27:22 +0800 Subject: [PATCH 13/79] ruff fix --- .../plugins/load/create_hero_version.py | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/client/ayon_core/plugins/load/create_hero_version.py b/client/ayon_core/plugins/load/create_hero_version.py index b18e874644..d741dafcce 100644 --- a/client/ayon_core/plugins/load/create_hero_version.py +++ b/client/ayon_core/plugins/load/create_hero_version.py @@ -64,7 +64,6 @@ class CreateHeroVersion(load.ProductLoaderPlugin): ) msgBox.exec_() - def load(self, context, name=None, namespace=None, options=None) -> None: """Load hero version from context (dict as in context.py).""" success = True @@ -168,7 +167,9 @@ class CreateHeroVersion(load.ProductLoaderPlugin): for repre_id in filtered_repre_ids: published_repres.pop(repre_id, None) if not published_repres: - raise RuntimeError("All published representations were filtered by name.") + raise RuntimeError( + "All published representations were filtered by name." + ) if src_version_entity is None: src_version_entity = self.version_from_representations( @@ -266,15 +267,18 @@ class CreateHeroVersion(load.ProductLoaderPlugin): shutil.rmtree(_backup_hero_publish_dir) backup_hero_publish_dir = _backup_hero_publish_dir break - except Exception: - _backup_hero_publish_dir = backup_hero_publish_dir + str(idx) + except Exception as exc: + _backup_hero_publish_dir = ( + backup_hero_publish_dir + str(idx) + ) if not os.path.exists(_backup_hero_publish_dir): backup_hero_publish_dir = _backup_hero_publish_dir break if idx > max_idx: raise AssertionError( - f"Backup folders are fully occupied to max index {max_idx}" - ) + "Backup folders are fully occupied to max index " + f"{max_idx}" + ) from exc idx += 1 try: os.rename(hero_publish_dir, backup_hero_publish_dir) @@ -324,13 +328,16 @@ class CreateHeroVersion(load.ProductLoaderPlugin): f"Single published file: {mapped_published_file} -> " f"{template_filled}" ) - # src_to_dst_file_paths being wrong else: collections, remainders = clique.assemble(published_files) if remainders or not collections or len(collections) > 1: raise Exception( - "Integrity error. Files of published representation is " - "combination of frame collections and single files.") + ( + "Integrity error. Files of published " + "representation is combination of frame " + "collections and single files." + ) + ) src_col = collections[0] frame_splitter = "_-_FRAME_SPLIT_-_" anatomy_data["frame"] = frame_splitter From 60558e440c93a167c1c58e8614178ff15e9af23f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 18 Aug 2025 13:16:17 +0200 Subject: [PATCH 14/79] Removed unnecessary field --- client/ayon_core/tools/push_to_project/control.py | 1 - 1 file changed, 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 eb985a3f8c..b52eeb5fad 100644 --- a/client/ayon_core/tools/push_to_project/control.py +++ b/client/ayon_core/tools/push_to_project/control.py @@ -40,7 +40,6 @@ class PushToContextController: self.set_source(project_name, version_id) - self._library_only = True # Events system def emit_event(self, topic, data=None, source=None): From 86130207186b40667ff436d077712958cecab31f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 18 Aug 2025 13:17:10 +0200 Subject: [PATCH 15/79] Change formatting --- client/ayon_core/tools/push_to_project/ui/window.py | 3 +-- 1 file changed, 1 insertion(+), 2 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 344295f177..b2f3983557 100644 --- a/client/ayon_core/tools/push_to_project/ui/window.py +++ b/client/ayon_core/tools/push_to_project/ui/window.py @@ -99,8 +99,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget): header_layout = QtWidgets.QHBoxLayout(header_widget) header_layout.setContentsMargins(0, 0, 0, 0) - header_layout.addWidget(header_label) - header_layout.addStretch(1) + header_layout.addWidget(header_label, 1) header_layout.addWidget(library_only_label, 0) header_layout.addWidget(library_only_checkbox, 0) From e7c0c8dab4fb91b010e587ac5f43694ac2beed50 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 18 Aug 2025 13:19:04 +0200 Subject: [PATCH 16/79] Removed unnecessary refresh --- 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 b2f3983557..38c343b023 100644 --- a/client/ayon_core/tools/push_to_project/ui/window.py +++ b/client/ayon_core/tools/push_to_project/ui/window.py @@ -410,6 +410,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self._projects_combobox.set_standard_filter_enabled(state) self._projects_combobox.refresh() + def _on_user_input_timer(self): folder_name_enabled = self._new_folder_name_enabled folder_name = self._new_folder_name_input_text From 92ecc854c9396e6bef33d9b68619320cc4ecf08e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 18 Aug 2025 13:23:51 +0200 Subject: [PATCH 17/79] Simplified _copy_version_thumbnail logic Used cached get_thumbnail_path --- .../tools/push_to_project/models/integrate.py | 50 ++++++++----------- 1 file changed, 21 insertions(+), 29 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 341858148b..b180892d62 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -22,6 +22,7 @@ from ayon_core.lib import ( source_hash, ) from ayon_core.lib.file_transaction import FileTransaction +from ayon_core.pipeline.thumbnails import get_thumbnail_path from ayon_core.settings import get_project_settings from ayon_core.pipeline import Anatomy from ayon_core.pipeline.version_start import get_versioning_start @@ -1150,36 +1151,27 @@ class ProjectPushItemProcess: ) def _copy_version_thumbnail(self): - version_thumbnail = ayon_api.get_version_thumbnail( - self._item.src_project_name, self._src_version_entity["id"]) - if not version_thumbnail or not version_thumbnail.id: + thumbnail_id = self._src_version_entity["thumbnailId"] + if not thumbnail_id: return - - temp_file_name = None - try: - with tempfile.NamedTemporaryFile(mode='w+b', delete=False) as fp: - fp.write(version_thumbnail.content) - temp_file_name = fp.name - - new_thumbnail_id = ayon_api.create_thumbnail( - self._item.dst_project_name, - temp_file_name - ) - - task_id = None - if self._task_info: - task_id = self._task_info["id"] - - self._operations.update_version( - project_name=self._item.dst_project_name, - version_id=self._version_entity["id"], - task_id=task_id, - thumbnail_id=new_thumbnail_id - ) - self._operations.commit() - finally: - if temp_file_name and os.path.exists(temp_file_name): - os.remove(temp_file_name) + path = get_thumbnail_path( + self._item.src_project_name, + "version", + self._src_version_entity["id"], + thumbnail_id + ) + if not path: + return + new_thumbnail_id = ayon_api.create_thumbnail( + self._item.dst_project_name, + path + ) + self._operations.update_version( + project_name=self._item.dst_project_name, + version_id=self._version_entity["id"], + thumbnail_id=new_thumbnail_id + ) + self._operations.commit() class IntegrateModel: From 3faee05cf6c05e089d8be8d42708493e7114933b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 18 Aug 2025 13:28:24 +0200 Subject: [PATCH 18/79] 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 19/79] 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 20/79] 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 21/79] 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 22/79] 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 23/79] 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 6c6be3508f5292324123fad320d78d98644b7de9 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 18 Aug 2025 13:53:14 +0200 Subject: [PATCH 24/79] Propagate taskId to limit json parse issue --- client/ayon_core/tools/push_to_project/models/integrate.py | 1 + 1 file changed, 1 insertion(+) 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..0c654a5495 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -1169,6 +1169,7 @@ class ProjectPushItemProcess: self._operations.update_version( project_name=self._item.dst_project_name, version_id=self._version_entity["id"], + task_id=self._version_entity.get("taskId"), thumbnail_id=new_thumbnail_id ) self._operations.commit() From 7860c7d875c539f52bfdd78c590ebc77a80c5af6 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 18 Aug 2025 13:55:49 +0200 Subject: [PATCH 25/79] Removed unnecessary refresh --- client/ayon_core/tools/push_to_project/ui/window.py | 2 -- 1 file changed, 2 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 38c343b023..495ef83ce6 100644 --- a/client/ayon_core/tools/push_to_project/ui/window.py +++ b/client/ayon_core/tools/push_to_project/ui/window.py @@ -408,8 +408,6 @@ 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_user_input_timer(self): folder_name_enabled = self._new_folder_name_enabled From d79bd055bcf771553076016b5cfd02e9ad4c5468 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 18 Aug 2025 14:03:08 +0200 Subject: [PATCH 26/79] Removed unnecessary import --- client/ayon_core/tools/push_to_project/models/integrate.py | 1 - 1 file changed, 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 0c654a5495..ac2f506112 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -3,7 +3,6 @@ import re import copy import itertools import sys -import tempfile import traceback import uuid From 930439ba12990611d8b01a7114fc4cdd5a77dab2 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 18 Aug 2025 14:26:28 +0200 Subject: [PATCH 27/79] Fix copy of frames based representations --- client/ayon_core/tools/push_to_project/models/integrate.py | 1 - 1 file changed, 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 ac2f506112..40c418e513 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -372,7 +372,6 @@ class ProjectPushRepreItem: resource_files.append(ResourceFile(filepath, relative_path)) continue - filepath = os.path.join(src_dirpath, basename) frame = None udim = None for item in src_basename_regex.finditer(basename): From 18ad64e2260124407cef7d01d37e5ceba0527f20 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 18 Aug 2025 14:43:29 +0200 Subject: [PATCH 28/79] Updated _copy_version_thumbnail logic --- .../tools/push_to_project/models/integrate.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 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 40c418e513..08fafcbf2d 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -484,7 +484,6 @@ class ProjectPushItemProcess: self._make_sure_version_exists() self._log_info("Prerequirements were prepared") self._integrate_representations() - self._copy_version_thumbnail() self._log_info("Integration finished") except PushToProjectError as exc: @@ -918,14 +917,19 @@ class ProjectPushItemProcess: task_name=self._task_info["name"], task_type=self._task_info["taskType"], product_type=product_type, - product_name=product_entity["name"] + product_name=product_entity["name"], ) existing_version_entity = ayon_api.get_version_by_name( project_name, version, product_id ) + thumbnail_id = self._copy_version_thumbnail() + # Update existing version if existing_version_entity: + updata_data = {"attrib": dst_attrib} + if thumbnail_id: + updata_data["thumbnailId"] = thumbnail_id self._operations.update_entity( project_name, "version", @@ -940,6 +944,7 @@ class ProjectPushItemProcess: version, product_id, attribs=dst_attrib, + thumbnail_id=thumbnail_id, ) self._operations.create_entity( project_name, "version", version_entity @@ -1160,17 +1165,10 @@ class ProjectPushItemProcess: ) if not path: return - new_thumbnail_id = ayon_api.create_thumbnail( + return ayon_api.create_thumbnail( self._item.dst_project_name, path ) - self._operations.update_version( - project_name=self._item.dst_project_name, - version_id=self._version_entity["id"], - task_id=self._version_entity.get("taskId"), - thumbnail_id=new_thumbnail_id - ) - self._operations.commit() class IntegrateModel: From c2b6204c0a1c10b463e9810009015b3e326df8ba Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 18 Aug 2025 15:09:17 +0200 Subject: [PATCH 29/79] Formatting change --- client/ayon_core/tools/push_to_project/control.py | 1 - 1 file changed, 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 b52eeb5fad..fb080d158b 100644 --- a/client/ayon_core/tools/push_to_project/control.py +++ b/client/ayon_core/tools/push_to_project/control.py @@ -40,7 +40,6 @@ class PushToContextController: self.set_source(project_name, version_id) - # Events system def emit_event(self, topic, data=None, source=None): """Use implemented event system to trigger event.""" From 8586431f17ac6503f604e7c1cf9be440f6483e6b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 18 Aug 2025 15:34:54 +0200 Subject: [PATCH 30/79] 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 31/79] 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 629794f6d64d4569fa03f48b13e5d2013697ff4e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 18 Aug 2025 16:41:08 +0200 Subject: [PATCH 32/79] Return formatting change Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- 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 08fafcbf2d..73a00a5cd9 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -1156,7 +1156,7 @@ class ProjectPushItemProcess: def _copy_version_thumbnail(self): thumbnail_id = self._src_version_entity["thumbnailId"] if not thumbnail_id: - return + return None path = get_thumbnail_path( self._item.src_project_name, "version", From 78cd71138337f54fef8544a34625cf5d06b9e53b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 18 Aug 2025 16:41:18 +0200 Subject: [PATCH 33/79] Return formatting change Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- 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 73a00a5cd9..197cefe819 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -1164,7 +1164,7 @@ class ProjectPushItemProcess: thumbnail_id ) if not path: - return + return None return ayon_api.create_thumbnail( self._item.dst_project_name, path From 8d4bcd1310c5c71e74785fbb11a421f8a8c45e72 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 18 Aug 2025 16:41:31 +0200 Subject: [PATCH 34/79] Typing Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- 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 197cefe819..c512d3ef68 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -1153,7 +1153,7 @@ class ProjectPushItemProcess: {"active": False} ) - def _copy_version_thumbnail(self): + def _copy_version_thumbnail(self) -> Optional[str]: thumbnail_id = self._src_version_entity["thumbnailId"] if not thumbnail_id: return None From 3b9b5e8063d3c44615ffa8cda43cd2fcd279cc98 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 18 Aug 2025 16:50:55 +0200 Subject: [PATCH 35/79] Removed run argument to not filter out project argument Current develop filters out 'project' cli argument as it is now used as key world for bundle per project implementation. --- client/ayon_core/plugins/load/push_to_library.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/plugins/load/push_to_library.py b/client/ayon_core/plugins/load/push_to_library.py index 22c10bbad7..3c7c7e503d 100644 --- a/client/ayon_core/plugins/load/push_to_library.py +++ b/client/ayon_core/plugins/load/push_to_library.py @@ -44,7 +44,6 @@ class PushToLibraryProject(load.ProductLoaderPlugin): version_id = context["version"]["id"] args = get_ayon_launcher_args( - "run", push_tool_script_path, "--project", project_name, "--version", version_id From ef34e9f79eebe12f3f1fdf845e1963a8dd83cd0b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 18 Aug 2025 16:53:33 +0200 Subject: [PATCH 36/79] Renamed loader --- .../plugins/load/{push_to_library.py => push_to_project.py} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename client/ayon_core/plugins/load/{push_to_library.py => push_to_project.py} (91%) diff --git a/client/ayon_core/plugins/load/push_to_library.py b/client/ayon_core/plugins/load/push_to_project.py similarity index 91% rename from client/ayon_core/plugins/load/push_to_library.py rename to client/ayon_core/plugins/load/push_to_project.py index 3c7c7e503d..dccac42444 100644 --- a/client/ayon_core/plugins/load/push_to_library.py +++ b/client/ayon_core/plugins/load/push_to_project.py @@ -6,8 +6,8 @@ from ayon_core.pipeline import load from ayon_core.pipeline.load import LoadError -class PushToLibraryProject(load.ProductLoaderPlugin): - """Export selected versions to folder structure from Template""" +class PushToProject(load.ProductLoaderPlugin): + """Export selected versions to different project""" is_multiple_contexts_compatible = True From bae1f64a91d8b4ec74bdb059048d42b96b4e346e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 18 Aug 2025 16:55:28 +0200 Subject: [PATCH 37/79] Fix typing --- client/ayon_core/tools/push_to_project/models/integrate.py | 1 + 1 file changed, 1 insertion(+) 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 c512d3ef68..89cd78cb0e 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -5,6 +5,7 @@ import itertools import sys import traceback import uuid +from typing import Optional import ayon_api from ayon_api.utils import create_entity_id From 12f415a639781b16c299e4b16edf128e9381e1a7 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 18 Aug 2025 17:01:42 +0200 Subject: [PATCH 38/79] 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 39/79] 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 40/79] 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 41/79] 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 42/79] 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 43/79] 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 44/79] 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 45/79] 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 46/79] 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 47/79] 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 48/79] 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 49/79] 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 50/79] 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 51/79] 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 52/79] 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 53/79] 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 54/79] 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 55/79] 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 56/79] 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 57/79] 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 58/79] 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 59/79] 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 60/79] 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 61/79] 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 5740b9f495f3c81f7eb405e83d81e804493a19c5 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 22 Aug 2025 16:04:27 +0800 Subject: [PATCH 62/79] update publishDir as being part of the instance_data --- .../plugins/load/create_hero_version.py | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/plugins/load/create_hero_version.py b/client/ayon_core/plugins/load/create_hero_version.py index d741dafcce..2d55069abf 100644 --- a/client/ayon_core/plugins/load/create_hero_version.py +++ b/client/ayon_core/plugins/load/create_hero_version.py @@ -75,7 +75,8 @@ class CreateHeroVersion(load.ProductLoaderPlugin): version = context.get("version") folder = context.get("folder") task_entity = ayon_api.get_task_by_id( - task_id=version.get("taskId"), project_name=project["name"]) + task_id=version.get("taskId"), project_name=project["name"] + ) anatomy = Anatomy(project["name"]) @@ -95,6 +96,7 @@ class CreateHeroVersion(load.ProductLoaderPlugin): "name": product["name"], "type": product["productType"], } + anatomy_data["version"] = version["version"] published_representations = {} for repre in repres: repre_anatomy = copy.deepcopy(anatomy_data) @@ -105,12 +107,26 @@ class CreateHeroVersion(load.ProductLoaderPlugin): "published_files": [f["path"] for f in repre.get("files", [])], "anatomy_data": repre_anatomy } - + publish_template_key = get_publish_template_name( + project_name, + context.get("hostName"), + product["productType"], + task_name=anatomy_data.get("task", {}).get("name"), + task_type=anatomy_data.get("task", {}).get("type"), + project_settings=context.get("project_settings", {}), + logger=self.log + ) + published_template_obj = anatomy.get_template_item( + "publish", publish_template_key, "directory" + ) + published_dir = os.path.normpath( + published_template_obj.format_strict(anatomy_data) + ) instance_data = { "productName": product["name"], "productType": product["productType"], "anatomyData": anatomy_data, - "publishDir": "", # TODO: Set to actual publish directory + "publishDir": published_dir, "published_representations": published_representations, "versionEntity": version, } @@ -199,7 +215,12 @@ class CreateHeroVersion(load.ProductLoaderPlugin): if file_path not in all_repre_file_paths: all_repre_file_paths.append(file_path) - instance_publish_dir = os.path.normpath(instance_data["publishDir"]) + publish_dir = instance_data.get("publishDir", "") + if not publish_dir: + raise RuntimeError( + "publishDir is empty in instance_data, cannot continue." + ) + instance_publish_dir = os.path.normpath(publish_dir) other_file_paths_mapping = [] for file_path in all_copied_files: if not file_path.startswith(instance_publish_dir): @@ -331,7 +352,7 @@ class CreateHeroVersion(load.ProductLoaderPlugin): else: collections, remainders = clique.assemble(published_files) if remainders or not collections or len(collections) > 1: - raise Exception( + raise RuntimeError( ( "Integrity error. Files of published " "representation is combination of frame " From 22e18cdfa253c7c4c53751bff2928e11206101fa Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 22 Aug 2025 16:07:02 +0800 Subject: [PATCH 63/79] add comment --- client/ayon_core/plugins/load/create_hero_version.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/plugins/load/create_hero_version.py b/client/ayon_core/plugins/load/create_hero_version.py index 2d55069abf..e9dbbfa652 100644 --- a/client/ayon_core/plugins/load/create_hero_version.py +++ b/client/ayon_core/plugins/load/create_hero_version.py @@ -107,6 +107,7 @@ class CreateHeroVersion(load.ProductLoaderPlugin): "published_files": [f["path"] for f in repre.get("files", [])], "anatomy_data": repre_anatomy } + # get the publish directory publish_template_key = get_publish_template_name( project_name, context.get("hostName"), From 89d0777bafcda1da9a7355a00da619cf8c326870 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 22 Aug 2025 16:21:37 +0800 Subject: [PATCH 64/79] copilot's feedback on - backup directory loop --- .../plugins/load/create_hero_version.py | 42 ++++++++----------- 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/client/ayon_core/plugins/load/create_hero_version.py b/client/ayon_core/plugins/load/create_hero_version.py index e9dbbfa652..adf9d5f669 100644 --- a/client/ayon_core/plugins/load/create_hero_version.py +++ b/client/ayon_core/plugins/load/create_hero_version.py @@ -167,13 +167,13 @@ class CreateHeroVersion(load.ProductLoaderPlugin): raise RuntimeError("Project anatomy does not have hero " f"template key: {template_key}") - print(f"Hero template: {hero_template.template}") + self.log.info(f"Hero template: {hero_template.template}") hero_publish_dir = self.get_publish_dir( instance_data, anatomy, template_key ) - print(f"Hero publish dir: {hero_publish_dir}") + self.log.info(f"Hero publish dir: {hero_publish_dir}") src_version_entity = instance_data.get("versionEntity") filtered_repre_ids = [] @@ -280,28 +280,22 @@ class CreateHeroVersion(load.ProductLoaderPlugin): backup_hero_publish_dir = None if os.path.exists(hero_publish_dir): - backup_hero_publish_dir = hero_publish_dir + ".BACKUP" + base_backup_dir = hero_publish_dir + ".BACKUP" max_idx = 10 - idx = 0 - _backup_hero_publish_dir = backup_hero_publish_dir - while os.path.exists(_backup_hero_publish_dir): - try: - shutil.rmtree(_backup_hero_publish_dir) - backup_hero_publish_dir = _backup_hero_publish_dir + # Find the first available backup directory name + for idx in range(max_idx + 1): + if idx == 0: + candidate_backup_dir = base_backup_dir + else: + candidate_backup_dir = f"{base_backup_dir}{idx}" + if not os.path.exists(candidate_backup_dir): + backup_hero_publish_dir = candidate_backup_dir break - except Exception as exc: - _backup_hero_publish_dir = ( - backup_hero_publish_dir + str(idx) - ) - if not os.path.exists(_backup_hero_publish_dir): - backup_hero_publish_dir = _backup_hero_publish_dir - break - if idx > max_idx: - raise AssertionError( - "Backup folders are fully occupied to max index " - f"{max_idx}" - ) from exc - idx += 1 + else: + raise AssertionError( + f"Backup folders are fully occupied to max index {max_idx}" + ) + try: os.rename(hero_publish_dir, backup_hero_publish_dir) except PermissionError: @@ -346,7 +340,7 @@ class CreateHeroVersion(load.ProductLoaderPlugin): src_to_dst_file_paths.append( (mapped_published_file, template_filled) ) - print( + self.log.info( f"Single published file: {mapped_published_file} -> " f"{template_filled}" ) @@ -379,7 +373,7 @@ class CreateHeroVersion(load.ProductLoaderPlugin): ) src_to_dst_file_paths.append((src_file, dst_file)) dst_paths.append(dst_file) - print( + self.log.info( f"Collection published file: {src_file} " f"-> {dst_file}" ) From 8d6f83ffa704fbc4d6bb4a73e6e065a631d3802d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 22 Aug 2025 11:30:48 +0200 Subject: [PATCH 65/79] restore saved painter --- client/ayon_core/tools/sceneinventory/select_version_dialog.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/ayon_core/tools/sceneinventory/select_version_dialog.py b/client/ayon_core/tools/sceneinventory/select_version_dialog.py index 68284ad1fe..18a39e495c 100644 --- a/client/ayon_core/tools/sceneinventory/select_version_dialog.py +++ b/client/ayon_core/tools/sceneinventory/select_version_dialog.py @@ -127,6 +127,7 @@ class SelectVersionComboBox(QtWidgets.QComboBox): status_text_rect.setLeft(icon_rect.right() + 2) if status_text_rect.width() <= 0: + painter.restore() return if status_text_rect.width() < metrics.width(status_name): @@ -144,6 +145,7 @@ class SelectVersionComboBox(QtWidgets.QComboBox): QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, status_name ) + painter.restore() def set_current_index(self, index): model = self._combo_view.model() From 16828011d22c633f0a2c9473c1f9ca2029397829 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 22 Aug 2025 13:40:26 +0200 Subject: [PATCH 66/79] 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 941d4aee9ea66b758bd337560fb48b6e38a1b1c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Tue, 26 Aug 2025 15:13:52 +0200 Subject: [PATCH 67/79] :recycle: add docstrings and hints --- .../plugins/load/create_hero_version.py | 165 +++++++++++++++--- 1 file changed, 139 insertions(+), 26 deletions(-) diff --git a/client/ayon_core/plugins/load/create_hero_version.py b/client/ayon_core/plugins/load/create_hero_version.py index adf9d5f669..aef0cf8863 100644 --- a/client/ayon_core/plugins/load/create_hero_version.py +++ b/client/ayon_core/plugins/load/create_hero_version.py @@ -1,10 +1,12 @@ - +"""Plugin to create hero version from selected context.""" +from __future__ import annotations import os import copy import shutil import errno import itertools from concurrent.futures import ThreadPoolExecutor +from typing import Any, Optional from speedcopy import copyfile import clique @@ -20,7 +22,17 @@ from ayon_core.pipeline.publish import get_publish_template_name from ayon_core.pipeline.template_data import get_template_data -def prepare_changes(old_entity, new_entity): +def prepare_changes(old_entity: dict, new_entity: dict) -> dict: + """Prepare changes dict for update entity operation. + + Args: + old_entity (dict): Existing entity data from database. + new_entity (dict): New entity data to compare against old. + + Returns: + dict: Changes to apply to old entity to make it like new entity. + + """ changes = {} for key in set(new_entity.keys()): if key == "attrib": @@ -48,19 +60,21 @@ class CreateHeroVersion(load.ProductLoaderPlugin): icon = "star" color = "#ffd700" - ignored_representation_names = [] + ignored_representation_names: list[str] = [] db_representation_context_keys = [ "project", "folder", "asset", "hierarchy", "task", "product", "subset", "family", "representation", "username", "user", "output" ] use_hardlinks = False - def message(self, text): + @staticmethod + def message(text: str) -> None: + """Show message box with text.""" msgBox = QtWidgets.QMessageBox() msgBox.setText(text) msgBox.setStyleSheet(style.load_stylesheet()) msgBox.setWindowFlags( - msgBox.windowFlags() | QtCore.Qt.FramelessWindowHint + msgBox.windowFlags() | QtCore.Qt.WindowType.FramelessWindowHint ) msgBox.exec_() @@ -143,8 +157,31 @@ class CreateHeroVersion(load.ProductLoaderPlugin): self.message( f"Failed to create hero version:\n{chr(10).join(errors)}") - def create_hero_version(self, instance_data, anatomy, context): - """Create hero version from instance data.""" + def create_hero_version( + self, + instance_data: dict[str, Any], + anatomy: Anatomy, + context: dict[str, Any]) -> None: + """Create hero version from instance data. + + Args: + instance_data (dict): Instance data with keys: + - productName (str): Name of the product. + - productType (str): Type of the product. + - anatomyData (dict): Anatomy data for templates. + - publishDir (str): Directory where the product is published. + - published_representations (dict): Published representations. + - versionEntity (dict, optional): Source version entity. + anatomy (Anatomy): Anatomy object for the project. + context (dict): Context data with keys: + - hostName (str): Name of the host application. + - project_settings (dict): Project settings. + + Raises: + RuntimeError: If any required data is missing or an error occurs + during the hero version creation process. + + """ published_repres = instance_data.get("published_representations") if not published_repres: raise RuntimeError("No published representations found.") @@ -158,7 +195,6 @@ class CreateHeroVersion(load.ProductLoaderPlugin): instance_data.get("anatomyData", {}).get("task", {}).get("type"), project_settings=context.get("project_settings", {}), hero=True, - logger=None ) hero_template = anatomy.get_template_item( "hero", template_key, "path", default=None @@ -197,12 +233,12 @@ class CreateHeroVersion(load.ProductLoaderPlugin): raise RuntimeError("Version 0 cannot have hero version.") all_copied_files = [] - transfers = instance_data.get("transfers", list()) + transfers = instance_data.get("transfers", []) for _src, dst in transfers: dst = os.path.normpath(dst) if dst not in all_copied_files: all_copied_files.append(dst) - hardlinks = instance_data.get("hardlinks", list()) + hardlinks = instance_data.get("hardlinks", []) for _src, dst in hardlinks: dst = os.path.normpath(dst) if dst not in all_copied_files: @@ -267,7 +303,6 @@ class CreateHeroVersion(load.ProductLoaderPlugin): instance_data["heroVersionEntity"] = new_hero_version old_repres_to_replace = {} - old_repres_to_delete = {} for repre_info in published_repres.values(): repre = repre_info["representation"] repre_name_low = repre["name"].lower() @@ -275,12 +310,10 @@ class CreateHeroVersion(load.ProductLoaderPlugin): old_repres_to_replace[repre_name_low] = ( old_repres_by_name.pop(repre_name_low) ) - if old_repres_by_name: - old_repres_to_delete = old_repres_by_name - + old_repres_to_delete = old_repres_by_name or {} backup_hero_publish_dir = None if os.path.exists(hero_publish_dir): - base_backup_dir = hero_publish_dir + ".BACKUP" + base_backup_dir = f"{hero_publish_dir}.BACKUP" max_idx = 10 # Find the first available backup directory name for idx in range(max_idx + 1): @@ -298,10 +331,11 @@ class CreateHeroVersion(load.ProductLoaderPlugin): try: os.rename(hero_publish_dir, backup_hero_publish_dir) - except PermissionError: + except PermissionError as e: raise AssertionError( "Could not create hero version because it is " - "not possible to replace current hero files.") + "not possible to replace current hero files." + ) from e try: src_to_dst_file_paths = [] @@ -445,14 +479,41 @@ class CreateHeroVersion(load.ProductLoaderPlugin): os.rename(backup_hero_publish_dir, hero_publish_dir) raise - def get_files_info(self, filepaths, anatomy): + def get_files_info( + self, filepaths: list[str], anatomy: Anatomy) -> list[dict]: + """Get list of file info dictionaries for given file paths. + + Args: + filepaths (list[str]): List of absolute file paths. + anatomy (Anatomy): Anatomy object for the project. + + Returns: + list[dict]: List of file info dictionaries. + + """ file_infos = [] for filepath in filepaths: file_info = self.prepare_file_info(filepath, anatomy) file_infos.append(file_info) return file_infos - def prepare_file_info(self, path, anatomy): + def prepare_file_info(self, path: str, anatomy: Anatomy) -> dict: + """Prepare file info dictionary for given path. + + Args: + path (str): Absolute file path. + anatomy (Anatomy): Anatomy object for the project. + + Returns: + dict: File info dictionary with keys: + - id (str): Unique identifier for the file. + - name (str): Base name of the file. + - path (str): Rootless file path. + - size (int): Size of the file in bytes. + - hash (str): Hash of the file content. + - hash_type (str): Type of the hash used. + + """ return { "id": create_entity_id(), "name": os.path.basename(path), @@ -462,7 +523,22 @@ class CreateHeroVersion(load.ProductLoaderPlugin): "hash_type": "op3", } - def get_publish_dir(self, instance_data, anatomy, template_key): + @staticmethod + def get_publish_dir( + instance_data: dict, + anatomy: Anatomy, + template_key: str) -> str: + """Get publish directory from instance data and anatomy. + + Args: + instance_data (dict): Instance data with "anatomyData" key. + anatomy (Anatomy): Anatomy object for the project. + template_key (str): Template key for the hero template. + + Returns: + str: Normalized publish directory path. + + """ template_data = copy.deepcopy(instance_data.get("anatomyData", {})) if "originalBasename" in instance_data: template_data["originalBasename"] = ( @@ -473,13 +549,34 @@ class CreateHeroVersion(load.ProductLoaderPlugin): ) return os.path.normpath(template_obj.format_strict(template_data)) - def get_rootless_path(self, anatomy, path): + @staticmethod + def get_rootless_path(anatomy: Anatomy, path: str) -> str: + """Get rootless path from absolute path. + + Args: + anatomy (Anatomy): Anatomy object for the project. + path (str): Absolute file path. + + Returns: + str: Rootless file path if root found, else original path. + + """ success, rootless_path = anatomy.find_root_template_from_path(path) if success: path = rootless_path return path - def copy_file(self, src_path, dst_path): + def copy_file(self, src_path: str, dst_path: str) -> None: + """Copy file from src to dst with creating directories. + + Args: + src_path (str): Source file path. + dst_path (str): Destination file path. + + Raises: + OSError: If copying or linking fails. + + """ dirname = os.path.dirname(dst_path) try: os.makedirs(dirname) @@ -495,23 +592,39 @@ class CreateHeroVersion(load.ProductLoaderPlugin): raise copyfile(src_path, dst_path) - def version_from_representations(self, project_name, repres): + @staticmethod + def version_from_representations( + project_name: str, repres: dict) -> Optional[dict[str, Any]]: + """Find version from representations. + + Args: + project_name (str): Name of the project. + repres (dict): Dictionary of representations info. + + Returns: + Optional[dict]: Version entity if found, else None. + + """ for repre_info in repres.values(): version = ayon_api.get_version_by_id( project_name, repre_info["representation"]["versionId"] ) if version: return version + return None - def current_hero_ents(self, project_name, version): + @staticmethod + def current_hero_ents( + project_name: str, + version: dict[str, Any]) -> tuple[Any, list[dict[str, Any]]]: hero_version = ayon_api.get_hero_version_by_product_id( project_name, version["productId"] ) if not hero_version: - return (None, []) + return None, [] hero_repres = list( ayon_api.get_representations( project_name, version_ids={hero_version["id"]} ) ) - return (hero_version, hero_repres) + return hero_version, hero_repres From 22d6819a322ed126ccde1a3f410bd080ba47e718 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 28 Aug 2025 10:51:39 +0200 Subject: [PATCH 68/79] 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 69/79] 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 70/79] 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 71/79] 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 72/79] 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 73/79] 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 74/79] 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) From a6a32b49fc176cb5039d47980c0252073f800350 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 2 Sep 2025 11:36:36 +0200 Subject: [PATCH 75/79] update opentimelineio to 0.17.0 --- client/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/pyproject.toml b/client/pyproject.toml index 6416d9b8e1..bccc0b9872 100644 --- a/client/pyproject.toml +++ b/client/pyproject.toml @@ -15,7 +15,7 @@ qtawesome = "0.7.3" [ayon.runtimeDependencies] aiohttp-middlewares = "^2.0.0" Click = "^8" -OpenTimelineIO = "0.16.0" +OpenTimelineIO = "0.17.0" opencolorio = "^2.3.2,<2.4.0" Pillow = "9.5.0" websocket-client = ">=0.40.0,<2" From c3b8f76501f5327343f98f5a5f106e264d34b06b Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 3 Sep 2025 20:18:44 +0800 Subject: [PATCH 76/79] max hosts for pre ocio hook so that the environment variable for ocio would be collected accuratly --- client/ayon_core/hooks/pre_ocio_hook.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hooks/pre_ocio_hook.py b/client/ayon_core/hooks/pre_ocio_hook.py index 85fcef47f2..be086dae65 100644 --- a/client/ayon_core/hooks/pre_ocio_hook.py +++ b/client/ayon_core/hooks/pre_ocio_hook.py @@ -14,7 +14,7 @@ class OCIOEnvHook(PreLaunchHook): "fusion", "blender", "aftereffects", - "3dsmax", + "max", "houdini", "maya", "nuke", From 14ab55e7ee75f25a34ccf1aa54865ef4f5cbe37e Mon Sep 17 00:00:00 2001 From: Ynbot Date: Fri, 5 Sep 2025 13:40:16 +0000 Subject: [PATCH 77/79] [Automated] Add generated package files from main --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index f2aa94020f..8eb2aa68f8 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.5.3+dev" +__version__ = "1.6.0" diff --git a/package.py b/package.py index 4393b7be40..37c3133eb0 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.5.3+dev" +version = "1.6.0" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index ee6c35b50b..302d249cca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.5.3+dev" +version = "1.6.0" description = "" authors = ["Ynput Team "] readme = "README.md" From 95a143ea4602b0fb12cefa9d0a1bd8911c799084 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Fri, 5 Sep 2025 13:40:54 +0000 Subject: [PATCH 78/79] [Automated] Update version in package.py for develop --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index 8eb2aa68f8..9ca5e1bc30 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.6.0" +__version__ = "1.6.0+dev" diff --git a/package.py b/package.py index 37c3133eb0..e430524dd5 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.6.0" +version = "1.6.0+dev" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 302d249cca..9a62a408ba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.6.0" +version = "1.6.0+dev" description = "" authors = ["Ynput Team "] readme = "README.md" From a269244e78c56b049cfc381bd0a927f2373f7f1d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 5 Sep 2025 13:41:47 +0000 Subject: [PATCH 79/79] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index ce5982969c..24c2b568b3 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to AYON Tray options: + - 1.6.0 - 1.5.3 - 1.5.2 - 1.5.1