diff --git a/client/ayon_core/plugins/load/push_to_project.py b/client/ayon_core/plugins/load/push_to_project.py index dccac42444..0b218d6ea1 100644 --- a/client/ayon_core/plugins/load/push_to_project.py +++ b/client/ayon_core/plugins/load/push_to_project.py @@ -28,10 +28,12 @@ class PushToProject(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] + folder_ids = set( + 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, @@ -39,13 +41,16 @@ class PushToProject(load.ProductLoaderPlugin): "push_to_project", "main.py" ) + project_name = filtered_contexts[0]["project"]["name"] - project_name = context["project"]["name"] - version_id = context["version"]["id"] + version_ids = { + context["version"]["id"] + for context in filtered_contexts + } args = get_ayon_launcher_args( push_tool_script_path, "--project", project_name, - "--version", version_id + "--versions", ",".join(version_ids) ) run_detached_process(args) diff --git a/client/ayon_core/tools/push_to_project/control.py b/client/ayon_core/tools/push_to_project/control.py index fb080d158b..b4e0d56dfd 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,10 +14,11 @@ from .models import ( UserPublishValuesModel, IntegrateModel, ) +from .models.integrate import ProjectPushItemProcess 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) @@ -27,18 +29,20 @@ 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_version_entities = [] + self._src_product_entities = {} self._src_label = None self._submission_enabled = False self._process_thread = None self._process_item_id = None - self.set_source(project_name, version_id) + self._use_original_name = False + + self.set_source(project_name, version_ids) # Events system def emit_event(self, topic, data=None, source=None): @@ -51,38 +55,47 @@ 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. + There is currently assumption that tool is working on products of same + folder. + Args: project_name (Union[str, None]): Source project name. - version_id (Union[str, None]): Source version id. + version_ids (Optional[list[str]]): Version ids. """ - + if not project_name or not version_ids: + return 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 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 = [] + 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)) + + 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"] ) @@ -97,15 +110,18 @@ 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 + 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() 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) @@ -113,7 +129,7 @@ class PushToContextController: "source.changed", { "project_name": project_name, - "version_id": version_id + "version_ids": self._src_version_ids } ) @@ -142,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() @@ -165,8 +189,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): @@ -176,29 +201,33 @@ 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, + use_original_name=self._use_original_name, + ) + 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: @@ -207,7 +236,7 @@ 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 @@ -215,14 +244,21 @@ class PushToContextController: return "Source is invalid" 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 version_entity in self._src_version_entities: + product_entity = self._src_product_entities.get( + version_entity["productId"] + ) + 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 @@ -256,7 +292,8 @@ class PushToContextController: def _get_src_variant(self): project_name = self._src_project_name - version_entity = self._src_version_entity + # 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( project_name, version_ids={version_entity["id"]} @@ -264,9 +301,12 @@ class PushToContextController: task_name, task_type = self._get_task_info_from_repre_entities( task_entities, repre_entities ) + product_entity = self._src_product_entities.get( + version_entity["productId"] + ) project_settings = get_project_settings(project_name) - product_type = self._src_product_entity["productType"] + product_type = product_entity["productType"] template = get_product_name_template( self._src_project_name, product_type, @@ -300,7 +340,7 @@ class PushToContextController: print("Failed format", exc) return "" - product_name = self._src_product_entity["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)) @@ -314,9 +354,6 @@ class PushToContextController: return product_name def _check_submit_validations(self): - if not self._user_values.is_valid: - return False - if not self._selection_model.get_selected_project_name(): return False @@ -325,6 +362,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): @@ -338,13 +382,14 @@ 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) + 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_id == self._process_item_id: - self._process_item_id = None + + if process_item_ids is self._process_item_ids: + self._process_item_ids = [] def _emit_event(self, topic, data=None): if data is None: diff --git a/client/ayon_core/tools/push_to_project/main.py b/client/ayon_core/tools/push_to_project/main.py index a6ff38c16f..d3c9d3a537 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.split(",")) if __name__ == "__main__": 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 89cd78cb0e..ef49838152 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 @@ -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 @@ -313,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: @@ -330,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"): @@ -338,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 @@ -359,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) @@ -390,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()): @@ -400,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)) @@ -493,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) ) @@ -817,31 +824,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 " # 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) self._log_info( f"Push will be integrating to product with name '{product_name}'" @@ -1012,10 +1022,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( + copy.deepcopy(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] @@ -1031,7 +1049,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 @@ -1054,7 +1071,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) @@ -1141,7 +1157,7 @@ class ProjectPushItemProcess: self._item.dst_project_name, "representation", entity_id, - changes + changes, ) existing_repre_names = set(existing_repres_by_low_name.keys()) @@ -1171,6 +1187,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): @@ -1194,6 +1232,7 @@ class IntegrateModel: comment, new_folder_name, dst_version, + use_original_name ): """Create new item for integration. @@ -1207,6 +1246,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 @@ -1222,7 +1262,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 @@ -1240,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 495ef83ce6..f382ccce64 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) @@ -205,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 " + "variant values." + ) overlay_close_btn = QtWidgets.QPushButton( "Close", overlay_btns_widget @@ -250,6 +257,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) @@ -298,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 @@ -326,7 +336,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_id = None self._variant_is_valid = None self._folder_is_valid = None @@ -337,17 +346,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_ids (Union[str, None]): comma separated 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) @@ -362,10 +371,12 @@ class PushToContextSelectWindow(QtWidgets.QWidget): 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"] ) - + self._controller._invalidate() self._projects_combobox.refresh() def _on_first_show(self): @@ -409,6 +420,10 @@ class PushToContextSelectWindow(QtWidgets.QWidget): state = bool(state) self._projects_combobox.set_standard_filter_enabled(state) + def _on_original_names_change(self, state: int) -> None: + use_original_name = bool(state) + self._invalidate_use_original_names(use_original_name) + def _on_user_input_timer(self): folder_name_enabled = self._new_folder_name_enabled folder_name = self._new_folder_name_input_text @@ -471,17 +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._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): + """Checks if original names must be used. + + Invalidates Variant if necessary + """ + if self._controller.original_names_required(): + use_original_names = True + + 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) + def _on_submission_change(self, event): self._publish_btn.setEnabled(event["enabled"]) @@ -510,31 +535,43 @@ 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 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"] + # 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")