From 139706a62e7043626f16776be0bf8b20608793dd Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 28 Dec 2022 10:27:40 +0100 Subject: [PATCH 01/24] initial commit of window --- .../republisher_dialog/__init__.py | 0 .../republisher/republisher_dialog/window.py | 428 ++++++++++++++++++ .../republisher_processor/__init__.py | 0 .../republisher_processor/control.py | 24 + 4 files changed, 452 insertions(+) create mode 100644 openpype/tools/republisher/republisher_dialog/__init__.py create mode 100644 openpype/tools/republisher/republisher_dialog/window.py create mode 100644 openpype/tools/republisher/republisher_processor/__init__.py create mode 100644 openpype/tools/republisher/republisher_processor/control.py diff --git a/openpype/tools/republisher/republisher_dialog/__init__.py b/openpype/tools/republisher/republisher_dialog/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openpype/tools/republisher/republisher_dialog/window.py b/openpype/tools/republisher/republisher_dialog/window.py new file mode 100644 index 0000000000..e268e1f6fb --- /dev/null +++ b/openpype/tools/republisher/republisher_dialog/window.py @@ -0,0 +1,428 @@ +import collections + +from qtpy import QtWidgets, QtGui, QtCore + +from openpype.client import get_projects, get_assets +from openpype.lib.events import EventSystem + + +PROJECT_NAME_ROLE = QtCore.Qt.UserRole + 1 +ASSET_NAME_ROLE = QtCore.Qt.UserRole + 2 +ASSET_ICON_ROLE = QtCore.Qt.UserRole + 3 +ASSET_ID_ROLE = QtCore.Qt.UserRole + 4 + + +class AssetItem: + def __init__(self, entity_id, name, icon, parent_id): + self.id = entity_id + self.name = name + self.icon = icon + self.parent_id = parent_id + + @classmethod + def from_doc(cls, asset_doc): + parent_id = asset_doc["data"].get("visualParent") + if parent_id is not None: + parent_id = str(parent_id) + return cls( + str(asset_doc["_id"]), + asset_doc["name"], + asset_doc["data"].get("icon"), + parent_id + ) + + + +class EntitiesModel: + def __init__(self, event_system): + self._event_system = event_system + self._projects = None + self._assets_by_project = {} + self._tasks_by_asset_id = collections.defaultdict(dict) + + def has_cached_projects(self): + return self._projects is None + + def has_cached_assets(self, project_name): + if not project_name: + return True + return project_name in self._assets_by_project + + def has_cached_tasks(self, project_name): + if not project_name: + return True + return project_name in self._assets_by_project + + def get_projects(self): + if self._projects is not None: + return list(self._projects) + + self.refresh_projects() + + def get_assets(self, project_name): + if project_name in self._assets_by_project: + return dict(self._assets_by_project[project_name]) + self.refresh_assets(project_name) + return [] + + def get_tasks(self, project_name, asset_id): + output = [] + if not project_name or not asset_id: + return output + + if project_name not in self._assets_by_project: + self.refresh_assets(project_name) + return output + + asset_docs = self._assets_by_project[project_name] + asset_doc = asset_docs.get(asset_id) + if not asset_doc: + return output + + for task, _task_info in asset_doc["data"]["tasks"].items(): + output.append(task) + return output + + def refresh_projects(self): + self._projects = None + self._event_system.emit( + "projects.refresh.started", {}, "entities.model" + ) + self._projects = [project["name"] for project in get_projects()] + self._event_system.emit( + "projects.refresh.finished", {}, "entities.model" + ) + + def refresh_assets(self, project_name): + self._event_system.emit( + "assets.refresh.started", + {"project_name": project_name}, + "entities.model" + ) + asset_docs = [] + if project_name: + asset_docs = get_assets(project_name) + asset_items_by_id = {} + for asset_doc in asset_docs: + asset_item = AssetItem.from_doc(asset_doc) + asset_items_by_id[asset_item.id] = asset_item + self._assets_by_project[project_name] = asset_items_by_id + self._event_system.emit( + "assets.refresh.finished", + {"project_name": project_name}, + "entities.model" + ) + + +class SelectionModel: + def __init__(self, event_system): + self._event_system = event_system + + self.project_name = None + self.asset_id = None + self.task_name = None + + def select_project(self, project_name): + if self.project_name == project_name: + return + + self.project_name = project_name + self.asset_id = None + self.task_name = None + self._event_system.emit( + "project.changed", + {"project_name": project_name}, + "selection.model" + ) + + def select_asset(self, asset_id): + if self.asset_id == asset_id: + return + self.asset_id = asset_id + self.task_name = None + self._event_system.emit( + "asset.changed", + { + "project_name": self.project_name, + "asset_id": asset_id + }, + "selection.model" + ) + + def select_task(self, task_name): + if self.task_name == task_name: + return + self.task_name = task_name + self._event_system.emit( + "task.changed", + { + "project_name": self.project_name, + "asset_id": self.asset_id, + "task_name": task_name + }, + "selection.model" + ) + + +class RepublisherDialogController: + def __init__(self): + event_system = EventSystem() + entities_model = EntitiesModel(event_system) + selection_model = SelectionModel(event_system) + + self._event_system = event_system + self._entities_model = entities_model + self._selection_model = selection_model + + self.dst_project_name = None + self.dst_asset_id = None + self.dst_task_name = None + + @property + def event_system(self): + return self._event_system + + @property + def model(self): + return self._entities_model + + @property + def selection_model(self): + return self._selection_model + + +class ProjectsModel(QtGui.QStandardItemModel): + empty_text = "< Empty >" + refreshing_text = "< Refreshing >" + select_project_text = "< Select Project >" + + def __init__(self, controller): + super().__init__() + self._controller = controller + + self.event_system.add_callback( + "projects.refresh.finished", self._on_refresh_finish + ) + + placeholder_item = QtGui.QStandardItem(self.empty_text) + + root_item = self.invisibleRootItem() + root_item.appendRows([placeholder_item]) + items = {None: placeholder_item} + + self._placeholder_item = placeholder_item + self._items = items + + @property + def event_system(self): + return self._controller.event_system + + def _on_refresh_finish(self): + root_item = self.invisibleRootItem() + project_names = self._controller.model.get_projects() + + if not project_names: + placeholder_text = self.empty_text + else: + placeholder_text = self.select_project_text + self._placeholder_item.setData(placeholder_text, QtCore.Qt.DisplayRole) + + new_items = [] + if None not in self._items: + new_items.append(self._placeholder_item) + + current_project_names = set(self._items.keys()) + for project_name in current_project_names - set(project_names): + if project_name is None: + continue + item = self._items.pop(project_name) + root_item.removeRow(item.row()) + + for project_name in project_names: + if project_name in self._items: + continue + item = QtGui.QStandardItem(project_name) + item.setData(project_name, PROJECT_NAME_ROLE) + new_items.append(item) + + if new_items: + root_item.appendRows(new_items) + + +class AssetsModel(QtGui.QStandardItemModel): + empty_text = "< Empty >" + + def __init__(self, controller): + super().__init__() + self._controller = controller + + items = {} + + placeholder_item = QtGui.QStandardItem(self.empty_text) + + root_item = self.invisibleRootItem() + root_item.appendRows([placeholder_item]) + items[None] = placeholder_item + + self.event_system.add_callback( + "project.changed", self._on_project_change + ) + self.event_system.add_callback( + "assets.refresh.started", self._on_refresh_start + ) + self.event_system.add_callback( + "assets.refresh.finished", self._on_refresh_finish + ) + + self._items = {} + + self._placeholder_item = placeholder_item + + @property + def event_system(self): + return self._controller.event_system + + def _clear(self): + placeholder_in = False + root_item = self.invisibleRootItem() + for row in reversed(range(root_item.rowCount())): + item = root_item.child(row) + asset_id = item.data(ASSET_ID_ROLE) + if asset_id is None: + placeholder_in = True + continue + root_item.removeRow(item.row()) + + for key in tuple(self._items.keys()): + if key is not None: + self._items.pop(key) + + if not placeholder_in: + root_item.appendRows([self._placeholder_item]) + self._items[None] = self._placeholder_item + + def _on_project_change(self, event): + self._clear() + + def _on_refresh_start(self, event): + pass + + def _on_refresh_finish(self, event): + event_project_name = event["project_name"] + project_name = self._controller.selection_model.project_name + print("finished", event_project_name, project_name) + if event_project_name != project_name: + return + + if project_name is None: + if None not in self._items: + self._clear() + return + + asset_items_by_id = self._controller.model.get_assets(project_name) + if not asset_items_by_id: + self._clear() + return + + assets_by_parent_id = collections.defaultdict(list) + for asset_item in asset_items_by_id.values(): + assets_by_parent_id[asset_item.parent_id].append(asset_item) + + root_item = self.invisibleRootItem() + if None in self._items: + self._items.pop(None) + root_item.takeRow(self._placeholder_item.row()) + + items_to_remove = set(self._items) - set(asset_items_by_id.keys()) + + hierarchy_queue = collections.deque() + hierarchy_queue.append((None, root_item)) + while hierarchy_queue: + parent_id, parent_item = hierarchy_queue.popleft() + new_items = [] + for asset_item in assets_by_parent_id[parent_id]: + item = self._items.get(asset_item.id) + if item is None: + item = QtGui.QStandardItem() + new_items.append(item) + self._items[asset_item.id] = item + + elif item.parent() is not parent_item: + new_items.append(item) + + item.setData(asset_item.name, QtCore.Qt.DisplayRole) + item.setData(asset_item.id, ASSET_ID_ROLE) + item.setData(asset_item.icon, ASSET_ICON_ROLE) + + if new_items: + parent_item.appendRows(new_items) + + for item_id in items_to_remove: + item = self._items.pop(item_id, None) + if item is None: + continue + parent = item.parent() + if parent is not None: + parent.removeRow(item.row()) + + +class RepublisherDialogWindow(QtWidgets.QWidget): + def __init__(self, controller=None): + super().__init__() + if controller is None: + controller = RepublisherDialogController() + self._controller = controller + + left_widget = QtWidgets.QWidget(self) + + project_combobox = QtWidgets.QComboBox(left_widget) + project_model = ProjectsModel(controller) + project_delegate = QtWidgets.QStyledItemDelegate() + project_combobox.setItemDelegate(project_delegate) + project_combobox.setModel(project_model) + + asset_view = QtWidgets.QTreeView(self) + asset_model = AssetsModel(controller) + asset_view.setModel(asset_model) + + left_layout = QtWidgets.QVBoxLayout(left_widget) + left_layout.addWidget(project_combobox, 0) + left_layout.addWidget(asset_view, 1) + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.addWidget(left_widget) + + self._project_combobox = project_combobox + self._project_model = project_model + self._project_delegate = project_delegate + + self._asset_view = asset_view + self._asset_model = asset_model + + self._first_show = True + + def showEvent(self, event): + super().showEvent(event) + if self._first_show: + self._first_show = False + self._controller.model.refresh_projects() + + +def main(): + app = QtWidgets.QApplication.instance() + if not app: + app = QtWidgets.QApplication([]) + + # TODO find way how to get these + project_name = None + representation_id = None + + # Show window dialog + window = RepublisherDialogWindow() + window.show() + + app.exec_() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/openpype/tools/republisher/republisher_processor/__init__.py b/openpype/tools/republisher/republisher_processor/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openpype/tools/republisher/republisher_processor/control.py b/openpype/tools/republisher/republisher_processor/control.py new file mode 100644 index 0000000000..dbaffe2ef7 --- /dev/null +++ b/openpype/tools/republisher/republisher_processor/control.py @@ -0,0 +1,24 @@ +import requests + + +class PublishItem: + def __init__( + self, + src_project_name, + src_representation_id, + dst_project_name, + dst_asset_id, + dst_task_name + ): + self.src_project_name = src_project_name + self.src_representation_id = src_representation_id + self.dst_project_name = dst_project_name + self.dst_asset_id = dst_asset_id + self.dst_task_name = dst_task_name + + +class RepublisherController: + def __init__(self): + pass + + From 77d26dd0232a65fc62853661d64f423797c53221 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 30 Dec 2022 14:52:48 +0100 Subject: [PATCH 02/24] separated controller and UI --- .../republisher/republisher_dialog/control.py | 225 +++++++++ .../republisher/republisher_dialog/window.py | 443 ++++++++++-------- 2 files changed, 473 insertions(+), 195 deletions(-) create mode 100644 openpype/tools/republisher/republisher_dialog/control.py diff --git a/openpype/tools/republisher/republisher_dialog/control.py b/openpype/tools/republisher/republisher_dialog/control.py new file mode 100644 index 0000000000..8c4e0313fe --- /dev/null +++ b/openpype/tools/republisher/republisher_dialog/control.py @@ -0,0 +1,225 @@ +import collections +from openpype.client import get_projects, get_assets +from openpype.lib.events import EventSystem + + +class AssetItem: + def __init__(self, entity_id, name, icon, parent_id): + self.id = entity_id + self.name = name + self.icon = icon + self.parent_id = parent_id + + @classmethod + def from_doc(cls, asset_doc): + parent_id = asset_doc["data"].get("visualParent") + if parent_id is not None: + parent_id = str(parent_id) + return cls( + str(asset_doc["_id"]), + asset_doc["name"], + asset_doc["data"].get("icon"), + parent_id + ) + + +class TaskItem: + def __init__(self, asset_id, name, task_type, short_name): + self.asset_id = asset_id + self.name = name + self.task_type = task_type + self.short_name = short_name + + @classmethod + def from_asset_doc(cls, asset_doc, project_doc): + asset_tasks = asset_doc["data"].get("tasks") or {} + project_task_types = project_doc["config"]["tasks"] + output = [] + for task_name, task_info in asset_tasks.items(): + task_type = task_info.get("type") + task_type_info = project_task_types.get(task_type) or {} + output.append(cls( + asset_doc["_id"], + task_name, + task_type, + task_type_info.get("short_name") + )) + return output + + +class EntitiesModel: + def __init__(self, event_system): + self._event_system = event_system + self._project_names = None + self._project_docs_by_name = {} + self._assets_by_project = {} + self._tasks_by_asset_id = collections.defaultdict(dict) + + def has_cached_projects(self): + return self._project_names is None + + def has_cached_assets(self, project_name): + if not project_name: + return True + return project_name in self._assets_by_project + + def has_cached_tasks(self, project_name): + return self.has_cached_assets(project_name) + + def get_projects(self): + if self._project_names is None: + self.refresh_projects() + return list(self._project_names) + + def get_assets(self, project_name): + if project_name not in self._assets_by_project: + self.refresh_assets(project_name) + return dict(self._assets_by_project[project_name]) + + def get_tasks(self, project_name, asset_id): + if not project_name or not asset_id: + return [] + + if project_name not in self._tasks_by_asset_id: + self.refresh_assets(project_name) + + all_task_items = self._tasks_by_asset_id[project_name] + asset_task_items = all_task_items.get(asset_id) + return list(asset_task_items) + + def refresh_projects(self, force=False): + self._event_system.emit( + "projects.refresh.started", {}, "entities.model" + ) + if force or self._project_names is None: + project_names = [] + project_docs_by_name = {} + for project_doc in get_projects(): + project_name = project_doc["name"] + project_names.append(project_name) + project_docs_by_name[project_name] = project_doc + self._project_names = project_names + self._project_docs_by_name = project_docs_by_name + self._event_system.emit( + "projects.refresh.finished", {}, "entities.model" + ) + + def _refresh_assets(self, project_name): + asset_items_by_id = {} + task_items_by_asset_id = {} + self._assets_by_project[project_name] = asset_items_by_id + self._tasks_by_asset_id[project_name] = task_items_by_asset_id + if not project_name: + return + + project_doc = self._project_docs_by_name[project_name] + for asset_doc in get_assets(project_name): + asset_item = AssetItem.from_doc(asset_doc) + asset_items_by_id[asset_item.id] = asset_item + task_items_by_asset_id[asset_item.id] = ( + TaskItem.from_asset_doc(asset_doc, project_doc) + ) + + def refresh_assets(self, project_name, force=False): + self._event_system.emit( + "assets.refresh.started", + {"project_name": project_name}, + "entities.model" + ) + + if force or project_name not in self._assets_by_project: + self._refresh_assets(project_name) + + self._event_system.emit( + "assets.refresh.finished", + {"project_name": project_name}, + "entities.model" + ) + + +class SelectionModel: + def __init__(self, event_system): + self._event_system = event_system + + self.project_name = None + self.asset_id = None + self.task_name = None + + def select_project(self, project_name): + if self.project_name == project_name: + return + + self.project_name = project_name + self._event_system.emit( + "project.changed", + {"project_name": project_name}, + "selection.model" + ) + + def select_asset(self, asset_id): + if self.asset_id == asset_id: + return + self.asset_id = asset_id + self._event_system.emit( + "asset.changed", + { + "project_name": self.project_name, + "asset_id": asset_id + }, + "selection.model" + ) + + def select_task(self, task_name): + if self.task_name == task_name: + return + self.task_name = task_name + self._event_system.emit( + "task.changed", + { + "project_name": self.project_name, + "asset_id": self.asset_id, + "task_name": task_name + }, + "selection.model" + ) + + +class RepublisherDialogController: + def __init__(self): + event_system = EventSystem() + entities_model = EntitiesModel(event_system) + selection_model = SelectionModel(event_system) + + self._event_system = event_system + self._entities_model = entities_model + self._selection_model = selection_model + + self.dst_project_name = None + self.dst_asset_id = None + self.dst_task_name = None + + event_system.add_callback("project.changed", self._on_project_change) + + @property + def event_system(self): + return self._event_system + + @property + def model(self): + return self._entities_model + + @property + def selection_model(self): + return self._selection_model + + def _on_project_change(self, event): + project_name = event["project_name"] + self.model.refresh_assets(project_name) + + def submit(self): + project_name = self.selection_model.project_name + asset_id = self.selection_model.asset_id + task_name = self.selection_model.task_name + self.dst_project_name = project_name + self.dst_asset_id = asset_id + self.dst_task_name = task_name diff --git a/openpype/tools/republisher/republisher_dialog/window.py b/openpype/tools/republisher/republisher_dialog/window.py index e268e1f6fb..593a95be17 100644 --- a/openpype/tools/republisher/republisher_dialog/window.py +++ b/openpype/tools/republisher/republisher_dialog/window.py @@ -2,193 +2,16 @@ import collections from qtpy import QtWidgets, QtGui, QtCore -from openpype.client import get_projects, get_assets -from openpype.lib.events import EventSystem +from openpype.style import load_stylesheet +from .control import RepublisherDialogController PROJECT_NAME_ROLE = QtCore.Qt.UserRole + 1 ASSET_NAME_ROLE = QtCore.Qt.UserRole + 2 ASSET_ICON_ROLE = QtCore.Qt.UserRole + 3 ASSET_ID_ROLE = QtCore.Qt.UserRole + 4 - - -class AssetItem: - def __init__(self, entity_id, name, icon, parent_id): - self.id = entity_id - self.name = name - self.icon = icon - self.parent_id = parent_id - - @classmethod - def from_doc(cls, asset_doc): - parent_id = asset_doc["data"].get("visualParent") - if parent_id is not None: - parent_id = str(parent_id) - return cls( - str(asset_doc["_id"]), - asset_doc["name"], - asset_doc["data"].get("icon"), - parent_id - ) - - - -class EntitiesModel: - def __init__(self, event_system): - self._event_system = event_system - self._projects = None - self._assets_by_project = {} - self._tasks_by_asset_id = collections.defaultdict(dict) - - def has_cached_projects(self): - return self._projects is None - - def has_cached_assets(self, project_name): - if not project_name: - return True - return project_name in self._assets_by_project - - def has_cached_tasks(self, project_name): - if not project_name: - return True - return project_name in self._assets_by_project - - def get_projects(self): - if self._projects is not None: - return list(self._projects) - - self.refresh_projects() - - def get_assets(self, project_name): - if project_name in self._assets_by_project: - return dict(self._assets_by_project[project_name]) - self.refresh_assets(project_name) - return [] - - def get_tasks(self, project_name, asset_id): - output = [] - if not project_name or not asset_id: - return output - - if project_name not in self._assets_by_project: - self.refresh_assets(project_name) - return output - - asset_docs = self._assets_by_project[project_name] - asset_doc = asset_docs.get(asset_id) - if not asset_doc: - return output - - for task, _task_info in asset_doc["data"]["tasks"].items(): - output.append(task) - return output - - def refresh_projects(self): - self._projects = None - self._event_system.emit( - "projects.refresh.started", {}, "entities.model" - ) - self._projects = [project["name"] for project in get_projects()] - self._event_system.emit( - "projects.refresh.finished", {}, "entities.model" - ) - - def refresh_assets(self, project_name): - self._event_system.emit( - "assets.refresh.started", - {"project_name": project_name}, - "entities.model" - ) - asset_docs = [] - if project_name: - asset_docs = get_assets(project_name) - asset_items_by_id = {} - for asset_doc in asset_docs: - asset_item = AssetItem.from_doc(asset_doc) - asset_items_by_id[asset_item.id] = asset_item - self._assets_by_project[project_name] = asset_items_by_id - self._event_system.emit( - "assets.refresh.finished", - {"project_name": project_name}, - "entities.model" - ) - - -class SelectionModel: - def __init__(self, event_system): - self._event_system = event_system - - self.project_name = None - self.asset_id = None - self.task_name = None - - def select_project(self, project_name): - if self.project_name == project_name: - return - - self.project_name = project_name - self.asset_id = None - self.task_name = None - self._event_system.emit( - "project.changed", - {"project_name": project_name}, - "selection.model" - ) - - def select_asset(self, asset_id): - if self.asset_id == asset_id: - return - self.asset_id = asset_id - self.task_name = None - self._event_system.emit( - "asset.changed", - { - "project_name": self.project_name, - "asset_id": asset_id - }, - "selection.model" - ) - - def select_task(self, task_name): - if self.task_name == task_name: - return - self.task_name = task_name - self._event_system.emit( - "task.changed", - { - "project_name": self.project_name, - "asset_id": self.asset_id, - "task_name": task_name - }, - "selection.model" - ) - - -class RepublisherDialogController: - def __init__(self): - event_system = EventSystem() - entities_model = EntitiesModel(event_system) - selection_model = SelectionModel(event_system) - - self._event_system = event_system - self._entities_model = entities_model - self._selection_model = selection_model - - self.dst_project_name = None - self.dst_asset_id = None - self.dst_task_name = None - - @property - def event_system(self): - return self._event_system - - @property - def model(self): - return self._entities_model - - @property - def selection_model(self): - return self._selection_model +TASK_NAME_ROLE = QtCore.Qt.UserRole + 5 +TASK_TYPE_ROLE = QtCore.Qt.UserRole + 6 class ProjectsModel(QtGui.QStandardItemModel): @@ -197,7 +20,7 @@ class ProjectsModel(QtGui.QStandardItemModel): select_project_text = "< Select Project >" def __init__(self, controller): - super().__init__() + super(ProjectsModel, self).__init__() self._controller = controller self.event_system.add_callback( @@ -249,20 +72,39 @@ class ProjectsModel(QtGui.QStandardItemModel): root_item.appendRows(new_items) +class ProjectProxyModel(QtCore.QSortFilterProxyModel): + def __init__(self): + super(ProjectProxyModel, self).__init__() + self._filter_empty_projects = False + + def set_filter_empty_project(self, filter_empty_projects): + if filter_empty_projects == self._filter_empty_projects: + return + self._filter_empty_projects = filter_empty_projects + self.invalidate() + + def filterAcceptsRow(self, row, parent): + if not self._filter_empty_projects: + return True + model = self.sourceModel() + source_index = model.index(row, self.filterKeyColumn(), parent) + if model.data(source_index, PROJECT_NAME_ROLE) is None: + return False + return True + + class AssetsModel(QtGui.QStandardItemModel): empty_text = "< Empty >" def __init__(self, controller): - super().__init__() + super(AssetsModel, self).__init__() self._controller = controller - items = {} - placeholder_item = QtGui.QStandardItem(self.empty_text) + placeholder_item.setFlags(QtCore.Qt.ItemIsEnabled) root_item = self.invisibleRootItem() root_item.appendRows([placeholder_item]) - items[None] = placeholder_item self.event_system.add_callback( "project.changed", self._on_project_change @@ -274,9 +116,10 @@ class AssetsModel(QtGui.QStandardItemModel): "assets.refresh.finished", self._on_refresh_finish ) - self._items = {} + self._items = {None: placeholder_item} self._placeholder_item = placeholder_item + self._last_project = None @property def event_system(self): @@ -302,6 +145,11 @@ class AssetsModel(QtGui.QStandardItemModel): self._items[None] = self._placeholder_item def _on_project_change(self, event): + project_name = event["project_name"] + if project_name == self._last_project: + return + + self._last_project = project_name self._clear() def _on_refresh_start(self, event): @@ -310,10 +158,10 @@ class AssetsModel(QtGui.QStandardItemModel): def _on_refresh_finish(self, event): event_project_name = event["project_name"] project_name = self._controller.selection_model.project_name - print("finished", event_project_name, project_name) if event_project_name != project_name: return + self._last_project = event["project_name"] if project_name is None: if None not in self._items: self._clear() @@ -334,7 +182,6 @@ class AssetsModel(QtGui.QStandardItemModel): root_item.takeRow(self._placeholder_item.row()) items_to_remove = set(self._items) - set(asset_items_by_id.keys()) - hierarchy_queue = collections.deque() hierarchy_queue.append((None, root_item)) while hierarchy_queue: @@ -344,6 +191,10 @@ class AssetsModel(QtGui.QStandardItemModel): item = self._items.get(asset_item.id) if item is None: item = QtGui.QStandardItem() + item.setFlags( + QtCore.Qt.ItemIsSelectable + | QtCore.Qt.ItemIsEnabled + ) new_items.append(item) self._items[asset_item.id] = item @@ -354,6 +205,8 @@ class AssetsModel(QtGui.QStandardItemModel): item.setData(asset_item.id, ASSET_ID_ROLE) item.setData(asset_item.icon, ASSET_ICON_ROLE) + hierarchy_queue.append((asset_item.id, item)) + if new_items: parent_item.appendRows(new_items) @@ -366,46 +219,246 @@ class AssetsModel(QtGui.QStandardItemModel): parent.removeRow(item.row()) +class TasksModel(QtGui.QStandardItemModel): + empty_text = "< Empty >" + + def __init__(self, controller): + super(TasksModel, self).__init__() + self._controller = controller + + placeholder_item = QtGui.QStandardItem(self.empty_text) + placeholder_item.setFlags(QtCore.Qt.ItemIsEnabled) + + root_item = self.invisibleRootItem() + root_item.appendRows([placeholder_item]) + + self.event_system.add_callback( + "project.changed", self._on_project_change + ) + self.event_system.add_callback( + "assets.refresh.finished", self._on_asset_refresh_finish + ) + self.event_system.add_callback( + "asset.changed", self._on_asset_change + ) + + self._items = {None: placeholder_item} + + self._placeholder_item = placeholder_item + self._last_project = None + + @property + def event_system(self): + return self._controller.event_system + + def _clear(self): + placeholder_in = False + root_item = self.invisibleRootItem() + for row in reversed(range(root_item.rowCount())): + item = root_item.child(row) + task_name = item.data(TASK_NAME_ROLE) + if task_name is None: + placeholder_in = True + continue + root_item.removeRow(item.row()) + + for key in tuple(self._items.keys()): + if key is not None: + self._items.pop(key) + + if not placeholder_in: + root_item.appendRows([self._placeholder_item]) + self._items[None] = self._placeholder_item + + def _on_project_change(self, event): + project_name = event["project_name"] + if project_name == self._last_project: + return + + self._last_project = project_name + self._clear() + + def _on_asset_refresh_finish(self, event): + self._refresh(event["project_name"]) + + def _on_asset_change(self, event): + self._refresh(event["project_name"]) + + def _refresh(self, new_project_name): + project_name = self._controller.selection_model.project_name + if new_project_name != project_name: + return + + self._last_project = project_name + if project_name is None: + if None not in self._items: + self._clear() + return + + asset_id = self._controller.selection_model.asset_id + task_items = self._controller.model.get_tasks( + project_name, asset_id + ) + if not task_items: + self._clear() + return + + root_item = self.invisibleRootItem() + if None in self._items: + self._items.pop(None) + root_item.takeRow(self._placeholder_item.row()) + + new_items = [] + task_names = set() + for task_item in task_items: + task_name = task_item.name + item = self._items.get(task_name) + if item is None: + item = QtGui.QStandardItem() + item.setFlags( + QtCore.Qt.ItemIsSelectable + | QtCore.Qt.ItemIsEnabled + ) + new_items.append(item) + self._items[task_name] = item + + item.setData(task_name, QtCore.Qt.DisplayRole) + item.setData(task_name, TASK_NAME_ROLE) + item.setData(task_item.task_type, TASK_TYPE_ROLE) + + if new_items: + root_item.appendRows(new_items) + + items_to_remove = set(self._items) - task_names + for item_id in items_to_remove: + item = self._items.pop(item_id, None) + if item is None: + continue + parent = item.parent() + if parent is not None: + parent.removeRow(item.row()) + + class RepublisherDialogWindow(QtWidgets.QWidget): def __init__(self, controller=None): - super().__init__() + super(RepublisherDialogWindow, self).__init__() if controller is None: controller = RepublisherDialogController() self._controller = controller - left_widget = QtWidgets.QWidget(self) + main_splitter = QtWidgets.QSplitter(self) + + left_widget = QtWidgets.QWidget(main_splitter) project_combobox = QtWidgets.QComboBox(left_widget) project_model = ProjectsModel(controller) + project_proxy = ProjectProxyModel() + project_proxy.setSourceModel(project_model) project_delegate = QtWidgets.QStyledItemDelegate() project_combobox.setItemDelegate(project_delegate) - project_combobox.setModel(project_model) + project_combobox.setModel(project_proxy) - asset_view = QtWidgets.QTreeView(self) + asset_view = QtWidgets.QTreeView(left_widget) + asset_view.setHeaderHidden(True) asset_model = AssetsModel(controller) asset_view.setModel(asset_model) left_layout = QtWidgets.QVBoxLayout(left_widget) + left_layout.setContentsMargins(0, 0, 0, 0) left_layout.addWidget(project_combobox, 0) left_layout.addWidget(asset_view, 1) + right_widget = QtWidgets.QWidget(main_splitter) + + task_view = QtWidgets.QListView(right_widget) + task_proxy = QtCore.QSortFilterProxyModel() + task_model = TasksModel(controller) + task_proxy.setSourceModel(task_model) + task_view.setModel(task_proxy) + + right_layout = QtWidgets.QVBoxLayout(right_widget) + right_layout.setContentsMargins(0, 0, 0, 0) + right_layout.addWidget(task_view, 1) + + main_splitter.addWidget(left_widget) + main_splitter.addWidget(right_widget) + + btns_widget = QtWidgets.QWidget(self) + close_btn = QtWidgets.QPushButton("Close", btns_widget) + select_btn = QtWidgets.QPushButton("Select", btns_widget) + + btns_layout = QtWidgets.QHBoxLayout(btns_widget) + btns_layout.setContentsMargins(0, 0, 0, 0) + btns_layout.addStretch(1) + btns_layout.addWidget(close_btn, 0) + btns_layout.addWidget(select_btn, 0) + main_layout = QtWidgets.QHBoxLayout(self) - main_layout.addWidget(left_widget) + main_layout.addWidget(main_splitter, 1) + main_layout.addWidget(btns_widget, 0) + + project_combobox.currentIndexChanged.connect(self._on_project_change) + asset_view.selectionModel().selectionChanged.connect( + self._on_asset_change + ) + task_view.selectionModel().selectionChanged.connect( + self._on_task_change + ) + select_btn.clicked.connect(self._on_select_click) + close_btn.clicked.connect(self._on_close_click) self._project_combobox = project_combobox self._project_model = project_model + self._project_proxy = project_proxy self._project_delegate = project_delegate self._asset_view = asset_view self._asset_model = asset_model + self._task_view = task_view + self._first_show = True def showEvent(self, event): - super().showEvent(event) + super(RepublisherDialogWindow, self).showEvent(event) if self._first_show: self._first_show = False self._controller.model.refresh_projects() + self.setStyleSheet(load_stylesheet()) + + def _on_project_change(self): + idx = self._project_combobox.currentIndex() + if idx < 0: + self._project_proxy.set_filter_empty_project(False) + return + + project_name = self._project_combobox.itemData(idx, PROJECT_NAME_ROLE) + self._project_proxy.set_filter_empty_project(project_name is not None) + self._controller.selection_model.select_project(project_name) + + def _on_asset_change(self): + indexes = self._asset_view.selectedIndexes() + index = next(iter(indexes), None) + asset_id = None + if index is not None: + model = self._asset_view.model() + asset_id = model.data(index, ASSET_ID_ROLE) + self._controller.selection_model.select_asset(asset_id) + + def _on_task_change(self): + indexes = self._task_view.selectedIndexes() + index = next(iter(indexes), None) + task_name = None + if index is not None: + model = self._task_view.model() + task_name = model.data(index, TASK_NAME_ROLE) + self._controller.selection_model.select_task(task_name) + + def _on_close_click(self): + self.close() + + def _on_select_click(self): + self._controller.submit() def main(): From ac67affcd3e0dcf5f99f8b43a2ea365f62f5a543 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 30 Dec 2022 14:53:52 +0100 Subject: [PATCH 03/24] initial commit of republisher logic --- .../republisher_processor/control.py | 410 +++++++++++++++++- 1 file changed, 407 insertions(+), 3 deletions(-) diff --git a/openpype/tools/republisher/republisher_processor/control.py b/openpype/tools/republisher/republisher_processor/control.py index dbaffe2ef7..8a235561d7 100644 --- a/openpype/tools/republisher/republisher_processor/control.py +++ b/openpype/tools/republisher/republisher_processor/control.py @@ -1,24 +1,428 @@ +import os +import re +import copy import requests +from openpype.client import ( + get_project, + get_asset_by_id, + get_subset_by_name, + get_representation_by_id, + get_representation_parents, +) +from openpype.lib import StringTemplate +from openpype.settings import get_project_settings +from openpype.pipeline.publish import get_publish_template_name +from openpype.pipeline.create import get_subset_name -class PublishItem: +class RepublishError(Exception): + pass + + +class RepublishItem: def __init__( self, src_project_name, src_representation_id, dst_project_name, dst_asset_id, - dst_task_name + dst_task_name, + dst_version=None ): self.src_project_name = src_project_name self.src_representation_id = src_representation_id self.dst_project_name = dst_project_name self.dst_asset_id = dst_asset_id self.dst_task_name = dst_task_name + self.dst_version = dst_version + self._id = "|".join([ + src_project_name, + src_representation_id, + dst_project_name, + dst_asset_id, + dst_task_name + ]) + + @property + def id(self): + return self._id + + def __repr__(self): + return "{} - {} -> {}/{}/{}".format( + self.src_project_name, + self.src_representation_id, + self.dst_project_name, + self.dst_asset_id, + self.dst_task_name + ) + + +class RepublishItemStatus: + def __init__( + self, + item, + failed=False, + finished=False, + error=None + ): + self._item = item + self._failed = failed + self._finished = finished + self._error = error + self._progress_messages = [] + self._last_message = None + + def get_failed(self): + return self._failed + + def set_failed(self, failed): + if failed == self._failed: + return + self._failed = failed + + def get_finished(self): + return self._finished + + def set_finished(self, finished): + if finished == self._finished: + return + self._finished = finished + + def get_error(self): + return self._error + + def set_error(self, error, failed=None): + if error == self._error: + return + self._error = error + if failed is None: + failed = error is not None + + if failed: + self.failed = failed + + failed = property(get_failed, set_failed) + finished = property(get_finished, set_finished) + error = property(get_error, set_error) + + def add_progress_message(self, message): + self._progress_messages.append(message) + self._last_message = message + print(message) + + @property + def last_message(self): + return self._last_message class RepublisherController: def __init__(self): - pass + self._items = {} + + def add_item(self, item): + if item.id in self._items: + raise RepublishError(f"Item is already in queue {item}") + self._items[item.id] = item + + def remote_item(self, item_id): + self._items.pop(item_id, None) + + def get_items(self): + return dict(self._items) +class SourceFile: + def __init__(self, path, frame=None, udim=None): + self.path = path + self.frame = frame + self.udim = udim + + def __repr__(self): + subparts = [self.__class__.__name__] + if self.frame is not None: + subparts.append("frame: {}".format(self.frame)) + if self.udim is not None: + subparts.append("UDIM: {}".format(self.udim)) + + return "<{}> '{}'".format(" - ".join(subparts), self.path) + + +class ResourceFile: + def __init__(self, path, relative_path): + self.path = path + self.relative_path = relative_path + + def __repr__(self): + return "<{}> '{}'".format(self.__class__.__name__, self.relative_path) + + +def get_source_files_with_frames(src_representation): + frame_placeholder = "__frame__" + udim_placeholder = "__udim__" + src_files = [] + resource_files = [] + template = src_representation["data"]["template"] + repre_context = src_representation["context"] + fill_repre_context = copy.deepcopy(repre_context) + if "frame" in fill_repre_context: + fill_repre_context["frame"] = frame_placeholder + + if "udim" in fill_repre_context: + fill_repre_context["udim"] = udim_placeholder + + fill_roots = fill_repre_context["root"] + for root_name in tuple(fill_roots.keys()): + fill_roots[root_name] = "{{root[{}]}}".format(root_name) + repre_path = StringTemplate.format_template(template, fill_repre_context) + repre_path = repre_path.replace("\\", "/") + src_dirpath, src_basename = os.path.split(repre_path) + src_basename = ( + re.escape(src_basename) + .replace(frame_placeholder, "(?P[0-9]+)") + .replace(udim_placeholder, "(?P[0-9]+)") + ) + src_basename_regex = re.compile("^{}$".format(src_basename)) + for file_info in src_representation["files"]: + filepath = file_info["path"].replace("\\", "/") + dirpath, basename = os.path.split(filepath) + if dirpath != src_dirpath or not src_basename_regex.match(basename): + relative_dir = dirpath.replace(src_dirpath, "") + if relative_dir: + relative_path = "/".join([relative_dir, basename]) + else: + relative_path = basename + resource_files.append(ResourceFile(filepath, relative_path)) + continue + + frame = None + udim = None + for item in src_basename_regex.finditer(basename): + group_name = item.lastgroup + value = item.group(group_name) + if group_name == "frame": + frame = int(value) + elif group_name == "udim": + udim = value + + src_files.append(SourceFile(filepath, frame, udim)) + + return src_files, resource_files + + +def get_source_files(src_representation): + repre_context = src_representation["context"] + if "frame" in repre_context or "udim" in repre_context: + return get_source_files_with_frames(src_representation) + + src_files = [] + resource_files = [] + template = src_representation["data"]["template"] + fill_repre_context = copy.deepcopy(repre_context) + fill_roots = fill_repre_context["root"] + for root_name in tuple(fill_roots.keys()): + fill_roots[root_name] = "{{root[{}]}}".format(root_name) + repre_path = StringTemplate.format_template(template, fill_repre_context) + repre_path = repre_path.replace("\\", "/") + src_dirpath = os.path.dirname(repre_path) + for file_info in src_representation["files"]: + filepath = file_info["path"] + if filepath == repre_path: + src_files.append(SourceFile(filepath)) + else: + dirpath, basename = os.path.split(filepath) + relative_dir = dirpath.replace(src_dirpath, "") + if relative_dir: + relative_path = "/".join([relative_dir, basename]) + else: + relative_path = basename + resource_files.append(ResourceFile(filepath, relative_path)) + return src_files, resource_files + + +def _republish_to( + item, + item_process, + src_representation, + src_representation_parents, + dst_asset_doc, + dst_task_info +): + """ + + Args: + item (RepublishItem): Item to process. + item_process (RepublishItemStatus): Item process information. + src_representation (Dict[str, Any]): Representation document. + src_representation_parents (Tuple[Any, Any, Any, Any]): Representation + parent documents. + dst_asset_doc (Dict[str, Any]): Asset document as destination of + publishing. + dst_task_info (Dict[str, str]): Task information with prepared + infromation from project config. + """ + + src_subset_doc = src_representation_parents[1] + family = src_subset_doc["data"].get("family") + if not family: + families = src_subset_doc["data"]["families"] + family = families[0] + + item_process.add_progress_message( + f"Republishing family '{family}' (Based on source subset)" + ) + # TODO how to define 'variant'? + variant = "Main" + # TODO where to get host? + host_name = "republisher" + project_settings = get_project_settings(item.dst_project_name) + + subset_name = get_subset_name( + family, + variant, + dst_task_info["name"], + dst_asset_doc, + project_name=item.dst_project_name, + host_name=host_name, + project_settings=project_settings + ) + item_process.add_progress_message(f"Final subset name is '{subset_name}'") + + template_name = get_publish_template_name( + item.dst_project_name, + host_name, + family, + dst_task_info["name"], + dst_task_info["type"], + project_settings=project_settings + ) + item_process.add_progress_message( + f"Using template '{template_name}' for integration" + ) + + src_files, resource_files = get_source_files(src_representation) + + +def _process_item(item, item_process): + # Query all entities source and destination + # - all of them are required for processing to exist + # --- Source entities --- + # Project - we just need validation of existence + src_project_name = item.src_project_name + src_project_doc = get_project(src_project_name, fields=["name"]) + if not src_project_doc: + item_process.error = ( + f"Source project '{src_project_name}' was not found" + ) + return + item_process.add_progress_message(f"Project '{src_project_name}' found") + + # Representation - contains information of source files and template data + src_representation_id = item.src_representation_id + src_representation = get_representation_by_id( + src_project_name, src_representation_id + ) + if not src_representation: + item_process.error = ( + f"Representation with id '{src_representation_id}'" + f" was not found in project '{src_project_name}'" + ) + return + item_process.add_progress_message( + f"Representation with id '{src_representation_id}' found" + f" in project '{src_project_name}'" + ) + + # --- Destination entities --- + dst_project_name = item.dst_project_name + dst_asset_id = item.dst_asset_id + dst_task_name = item.dst_task_name + + # Validate project existence + dst_project_doc = get_project(dst_project_name, fields=["name", "config"]) + if not dst_project_doc: + item_process.error = ( + f"Destination project '{dst_project_name}' was not found" + ) + return + item_process.add_progress_message(f"Project '{dst_project_name}' found") + + # Get asset document + dst_asset_doc = get_asset_by_id( + dst_project_name, + dst_asset_id + ) + if not dst_asset_doc: + item_process.error = ( + f"Destination asset with id '{dst_asset_id}'" + f" was not found in project '{dst_project_name}'" + ) + return + item_process.add_progress_message(( + f"Asset with id '{dst_asset_id}'" + f" found in project '{dst_project_name}'" + )) + + # Get task information from asset document + asset_tasks = dst_asset_doc.get("data", {}).get("tasks") or {} + task_info = asset_tasks.get(dst_task_name) + if not task_info: + item_process.error = ( + f"Destination task '{dst_task_name}'" + f" was not found on asset with id '{dst_asset_id}'" + f" in project '{dst_project_name}'" + ) + return + + item_process.add_progress_message(( + f"Task with name '{dst_task_name}'" + f" found on asset with id '{dst_asset_id}'" + f" in project '{dst_project_name}'" + )) + # Create copy of task info to avoid changing data in asset document + dst_task_info = copy.deepcopy(task_info) + dst_task_info["name"] = dst_task_name + # Fill rest of task information based on task type + task_type = dst_task_info["type"] + task_type_info = dst_project_doc["config"]["tasks"].get(task_type) + dst_task_info.update(task_type_info) + + src_representation_parents = get_representation_parents( + src_project_name, src_representation + ) + _republish_to( + item, + item_process, + src_representation, + src_representation_parents, + dst_asset_doc, + dst_task_info + ) + + +def fake_process(controller): + items = controller.get_items() + for item in items.values(): + item_process = RepublishItemStatus(item) + _process_item(item, item_process) + if item_process.failed: + print("Process failed") + else: + print("Process Finished") + + +def main(): + # NOTE For development purposes + controller = RepublisherController() + project_name = "" + representation_id = "" + dst_project_name = "" + dst_asset_id = "" + dst_task_name = "" + controller.add_item(RepublishItem( + project_name, + representation_id, + dst_project_name, + dst_asset_id, + dst_task_name + )) + fake_process(controller) \ No newline at end of file From 5d4be5750402f4657b5100a6de3f4438f68bbd14 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 5 Jan 2023 10:15:20 +0100 Subject: [PATCH 04/24] implemented new function 'get_subset_name_template' --- openpype/pipeline/create/__init__.py | 2 + openpype/pipeline/create/subset_name.py | 87 +++++++++++++++++-------- 2 files changed, 63 insertions(+), 26 deletions(-) diff --git a/openpype/pipeline/create/__init__.py b/openpype/pipeline/create/__init__.py index b0877d0a29..c89fb04c42 100644 --- a/openpype/pipeline/create/__init__.py +++ b/openpype/pipeline/create/__init__.py @@ -6,6 +6,7 @@ from .constants import ( from .subset_name import ( TaskNotSetError, + get_subset_name_template, get_subset_name, ) @@ -46,6 +47,7 @@ __all__ = ( "PRE_CREATE_THUMBNAIL_KEY", "TaskNotSetError", + "get_subset_name_template", "get_subset_name", "CreatorError", diff --git a/openpype/pipeline/create/subset_name.py b/openpype/pipeline/create/subset_name.py index f508263708..ed05dd6083 100644 --- a/openpype/pipeline/create/subset_name.py +++ b/openpype/pipeline/create/subset_name.py @@ -14,6 +14,53 @@ class TaskNotSetError(KeyError): super(TaskNotSetError, self).__init__(msg) +def get_subset_name_template( + project_name, + family, + task_name, + task_type, + host_name, + default_template=None, + project_settings=None +): + """Get subset name template based on passed context. + + Args: + project_name (str): Project on which the context lives. + family (str): Family (subset type) for which the subset name is + calculated. + host_name (str): Name of host in which the subset name is calculated. + task_name (str): Name of task in which context the subset is created. + task_type (str): Type of task in which context the subset is created. + default_template (Union[str, None]): Default template which is used if + settings won't find any matching possitibility. Constant + 'DEFAULT_SUBSET_TEMPLATE' is used if not defined. + project_settings (Union[Dict[str, Any], None]): Prepared settings for + project. Settings are queried if not passed. + """ + + if project_settings is None: + project_settings = get_project_settings(project_name) + tools_settings = project_settings["global"]["tools"] + profiles = tools_settings["creator"]["subset_name_profiles"] + filtering_criteria = { + "families": family, + "hosts": host_name, + "tasks": task_name, + "task_types": task_type + } + + matching_profile = filter_profiles(profiles, filtering_criteria) + template = None + if matching_profile: + template = matching_profile["template"] + + # Make sure template is set (matching may have empty string) + if not template: + template = default_template or DEFAULT_SUBSET_TEMPLATE + return template + + def get_subset_name( family, variant, @@ -37,9 +84,9 @@ def get_subset_name( Args: family (str): Instance family. - variant (str): In most of cases it is user input during creation. + variant (str): In most of the cases it is user input during creation. task_name (str): Task name on which context is instance created. - asset_doc (dict): Queried asset document with it's tasks in data. + asset_doc (dict): Queried asset document with its tasks in data. Used to get task type. project_name (str): Name of project on which is instance created. Important for project settings that are loaded. @@ -50,15 +97,15 @@ def get_subset_name( is not passed. dynamic_data (dict): Dynamic data specific for a creator which creates instance. - dbcon (AvalonMongoDB): Mongo connection to be able query asset document - if 'asset_doc' is not passed. + project_settings (Union[Dict[str, Any], None]): Prepared settings for + project. Settings are queried if not passed. """ if not family: return "" if not host_name: - host_name = os.environ["AVALON_APP"] + host_name = os.environ.get("AVALON_APP") # Use only last part of class family value split by dot (`.`) family = family.rsplit(".", 1)[-1] @@ -70,27 +117,15 @@ def get_subset_name( task_info = asset_tasks.get(task_name) or {} task_type = task_info.get("type") - # Get settings - if not project_settings: - project_settings = get_project_settings(project_name) - tools_settings = project_settings["global"]["tools"] - profiles = tools_settings["creator"]["subset_name_profiles"] - filtering_criteria = { - "families": family, - "hosts": host_name, - "tasks": task_name, - "task_types": task_type - } - - matching_profile = filter_profiles(profiles, filtering_criteria) - template = None - if matching_profile: - template = matching_profile["template"] - - # Make sure template is set (matching may have empty string) - if not template: - template = default_template or DEFAULT_SUBSET_TEMPLATE - + template = get_subset_name_template( + project_name, + family, + task_name, + task_type, + host_name, + default_template=default_template, + project_settings=project_settings + ) # Simple check of task name existence for template with {task} in # - missing task should be possible only in Standalone publisher if not task_name and "{task" in template.lower(): From 4c43acde9dc37247e1bb05d795cd139961a9cef6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 5 Jan 2023 10:18:06 +0100 Subject: [PATCH 05/24] changed select context dialog --- .../republisher/republisher_dialog/control.py | 427 +++++++++++++++++- .../republisher/republisher_dialog/window.py | 343 ++++++++++++-- 2 files changed, 717 insertions(+), 53 deletions(-) diff --git a/openpype/tools/republisher/republisher_dialog/control.py b/openpype/tools/republisher/republisher_dialog/control.py index 8c4e0313fe..242ca475e7 100644 --- a/openpype/tools/republisher/republisher_dialog/control.py +++ b/openpype/tools/republisher/republisher_dialog/control.py @@ -1,17 +1,42 @@ +import re import collections -from openpype.client import get_projects, get_assets + +from openpype.client import ( + get_projects, + get_assets, + get_asset_by_id, + get_subset_by_id, + get_version_by_id, + get_representations, +) +from openpype.settings import get_project_settings +from openpype.lib import prepare_template_data from openpype.lib.events import EventSystem +from openpype.pipeline.create import ( + SUBSET_NAME_ALLOWED_SYMBOLS, + get_subset_name_template, +) class AssetItem: - def __init__(self, entity_id, name, icon, parent_id): + def __init__( + self, + entity_id, + name, + icon_name, + icon_color, + parent_id, + has_children + ): self.id = entity_id self.name = name - self.icon = icon + self.icon_name = icon_name + self.icon_color = icon_color self.parent_id = parent_id + self.has_children = has_children @classmethod - def from_doc(cls, asset_doc): + def from_doc(cls, asset_doc, has_children=True): parent_id = asset_doc["data"].get("visualParent") if parent_id is not None: parent_id = str(parent_id) @@ -19,7 +44,9 @@ class AssetItem: str(asset_doc["_id"]), asset_doc["name"], asset_doc["data"].get("icon"), - parent_id + asset_doc["data"].get("color"), + parent_id, + has_children ) @@ -76,6 +103,9 @@ class EntitiesModel: self.refresh_assets(project_name) return dict(self._assets_by_project[project_name]) + def get_asset_by_id(self, project_name, asset_id): + return self._assets_by_project[project_name].get(asset_id) + def get_tasks(self, project_name, asset_id): if not project_name or not asset_id: return [] @@ -85,6 +115,8 @@ class EntitiesModel: all_task_items = self._tasks_by_asset_id[project_name] asset_task_items = all_task_items.get(asset_id) + if not asset_task_items: + return [] return list(asset_task_items) def refresh_projects(self, force=False): @@ -113,12 +145,25 @@ class EntitiesModel: return project_doc = self._project_docs_by_name[project_name] + asset_docs_by_parent_id = collections.defaultdict(list) for asset_doc in get_assets(project_name): - asset_item = AssetItem.from_doc(asset_doc) + parent_id = asset_doc["data"].get("visualParent") + asset_docs_by_parent_id[parent_id].append(asset_doc) + + hierarchy_queue = collections.deque() + for asset_doc in asset_docs_by_parent_id[None]: + hierarchy_queue.append(asset_doc) + + while hierarchy_queue: + asset_doc = hierarchy_queue.popleft() + children = asset_docs_by_parent_id[asset_doc["_id"]] + asset_item = AssetItem.from_doc(asset_doc, len(children) > 0) asset_items_by_id[asset_item.id] = asset_item task_items_by_asset_id[asset_item.id] = ( TaskItem.from_asset_doc(asset_doc, project_doc) ) + for child in children: + hierarchy_queue.append(child) def refresh_assets(self, project_name, force=False): self._event_system.emit( @@ -184,21 +229,330 @@ class SelectionModel: ) -class RepublisherDialogController: - def __init__(self): +class UserPublishValues: + """Helper object to validate values required for push to different project. + + Args: + event_system (EventSystem): Event system to catch and emit events. + new_asset_name (str): Name of new asset name. + variant (str): Variant for new subset name in new project. + """ + + asset_name_regex = re.compile("^[a-zA-Z0-9_.]+$") + variant_regex = re.compile("^[{}]+$".format(SUBSET_NAME_ALLOWED_SYMBOLS)) + + def __init__(self, event_system): + self._event_system = event_system + self._new_asset_name = None + self._variant = None + self._comment = None + self._is_variant_valid = False + self._is_new_asset_name_valid = False + + self.set_new_asset("") + self.set_variant("") + self.set_comment("") + + @property + def new_asset_name(self): + return self._new_asset_name + + @property + def variant(self): + return self._variant + + @property + def comment(self): + return self._comment + + @property + def is_variant_valid(self): + return self._is_variant_valid + + @property + def is_new_asset_name_valid(self): + return self._is_new_asset_name_valid + + @property + def is_valid(self): + return self.is_variant_valid and self.is_new_asset_name_valid + + def set_variant(self, variant): + if variant == self._variant: + return + + old_variant = self._variant + old_is_valid = self._is_variant_valid + + self._variant = variant + is_valid = False + if variant: + is_valid = self.variant_regex.match(variant) is not None + self._is_variant_valid = is_valid + + changes = { + key: {"new": new, "old": old} + for key, old, new in ( + ("variant", old_variant, variant), + ("is_valid", old_is_valid, is_valid) + ) + } + + self._event_system.emit( + "variant.changed", + { + "variant": variant, + "is_valid": self._is_variant_valid, + "changes": changes + }, + "user_values" + ) + + def set_new_asset(self, asset_name): + if self._new_asset_name == asset_name: + return + old_asset_name = self._new_asset_name + old_is_valid = self._is_new_asset_name_valid + self._new_asset_name = asset_name + is_valid = True + if asset_name: + is_valid = ( + self.asset_name_regex.match(asset_name) is not None + ) + self._is_new_asset_name_valid = is_valid + changes = { + key: {"new": new, "old": old} + for key, old, new in ( + ("new_asset_name", old_asset_name, asset_name), + ("is_valid", old_is_valid, is_valid) + ) + } + + self._event_system.emit( + "new_asset_name.changed", + { + "new_asset_name": self._new_asset_name, + "is_valid": self._is_new_asset_name_valid, + "changes": changes + }, + "user_values" + ) + + def set_comment(self, comment): + if comment == self._comment: + return + old_comment = self._comment + self._comment = comment + self._event_system.emit( + "comment.changed", + { + "new_asset_name": comment, + "changes": { + "comment": {"new": comment, "old": old_comment} + } + }, + "user_values" + ) + + +class PushToContextController: + def __init__(self, project_name=None, version_id=None): + self._src_project_name = None + self._src_version_id = None + self._src_asset_doc = None + self._src_subset_doc = None + self._src_version_doc = None + event_system = EventSystem() entities_model = EntitiesModel(event_system) selection_model = SelectionModel(event_system) + user_values = UserPublishValues(event_system) self._event_system = event_system self._entities_model = entities_model self._selection_model = selection_model - - self.dst_project_name = None - self.dst_asset_id = None - self.dst_task_name = None + self._user_values = user_values event_system.add_callback("project.changed", self._on_project_change) + event_system.add_callback("asset.changed", self._invalidate) + event_system.add_callback("variant.changed", self._invalidate) + event_system.add_callback("new_asset_name.changed", self._invalidate) + + self._submission_enabled = False + + self.set_source(project_name, version_id) + + def _get_task_info_from_repre_docs(self, asset_doc, repre_docs): + asset_tasks = asset_doc["data"].get("tasks") or {} + found_comb = [] + for repre_doc in repre_docs: + context = repre_doc["context"] + task_info = context.get("task") + if task_info is None: + continue + + task_name = None + task_type = None + if isinstance(task_info, str): + task_name = task_info + asset_task_info = asset_tasks.get(task_info) or {} + task_type = asset_task_info.get("type") + + elif isinstance(task_info, dict): + task_name = task_info.get("name") + task_type = task_info.get("type") + + if task_name and task_type: + return task_name, task_type + + if task_name: + found_comb.append((task_name, task_type)) + + for task_name, task_type in found_comb: + return task_name, task_type + return None, None + + def _get_src_variant(self): + project_name = self._src_project_name + version_doc = self._src_version_doc + asset_doc = self._src_asset_doc + repre_docs = get_representations( + project_name, version_ids=[version_doc["_id"]] + ) + task_name, task_type = self._get_task_info_from_repre_docs( + asset_doc, repre_docs + ) + + project_settings = get_project_settings(project_name) + subset_doc = self.src_subset_doc + family = subset_doc["data"].get("family") + if not family: + family = subset_doc["data"]["families"][0] + template = get_subset_name_template( + self._src_project_name, + family, + task_name, + task_type, + None, + project_settings=project_settings + ) + template_low = template.lower() + variant_placeholder = "{variant}" + if ( + variant_placeholder not in template_low + or (not task_name and "{task" in template_low) + ): + return "" + + idx = template_low.index(variant_placeholder) + template_s = template[:idx] + template_e = template[idx + len(variant_placeholder):] + fill_data = prepare_template_data({ + "family": family, + "task": task_name + }) + try: + subset_s = template_s.format(**fill_data) + subset_e = template_e.format(**fill_data) + except Exception as exc: + print("Failed format", exc) + return "" + + subset_name = self.src_subset_doc["name"] + if ( + (subset_s and not subset_name.startswith(subset_s)) + or (subset_e and not subset_name.endswith(subset_e)) + ): + return "" + + if subset_s: + subset_name = subset_name[len(subset_s):] + if subset_e: + subset_name = subset_name[:len(subset_e)] + return subset_name + + def set_source(self, project_name, version_id): + if ( + project_name == self._src_project_name + and version_id == self._src_version_id + ): + return + + self._src_project_name = project_name + self._src_version_id = version_id + asset_doc = None + subset_doc = None + version_doc = None + if project_name and version_id: + version_doc = get_version_by_id(project_name, version_id) + + if version_doc: + subset_doc = get_subset_by_id(project_name, version_doc["parent"]) + + if subset_doc: + asset_doc = get_asset_by_id(project_name, subset_doc["parent"]) + + self._src_asset_doc = asset_doc + self._src_subset_doc = subset_doc + self._src_version_doc = version_doc + if asset_doc: + self.user_values.set_new_asset(asset_doc["name"]) + variant = self._get_src_variant() + if variant: + self.user_values.set_variant(variant) + + comment = version_doc["data"].get("comment") + if comment: + self.user_values.set_comment(comment) + + self._event_system.emit( + "source.changed", { + "project_name": project_name, + "version_id": version_id + }, + "controller" + ) + + @property + def src_project_name(self): + return self._src_project_name + + @property + def src_version_id(self): + return self._src_version_id + + @property + def src_label(self): + if not self._src_project_name or not self._src_version_id: + return "Source is not defined" + + asset_doc = self.src_asset_doc + if not asset_doc: + return "Source is invalid" + + asset_path_parts = list(asset_doc["data"]["parents"]) + asset_path_parts.append(asset_doc["name"]) + asset_path = "/".join(asset_path_parts) + subset_doc = self.src_subset_doc + version_doc = self.src_version_doc + return "Source: {}/{}/{}/v{:0>3}".format( + self._src_project_name, + asset_path, + subset_doc["name"], + version_doc["name"] + ) + + @property + def src_version_doc(self): + return self._src_version_doc + + @property + def src_subset_doc(self): + return self._src_subset_doc + + @property + def src_asset_doc(self): + return self._src_asset_doc @property def event_system(self): @@ -212,11 +566,60 @@ class RepublisherDialogController: def selection_model(self): return self._selection_model + @property + def user_values(self): + return self._user_values + + @property + def submission_enabled(self): + return self._submission_enabled + def _on_project_change(self, event): project_name = event["project_name"] self.model.refresh_assets(project_name) + self._invalidate() + + def _invalidate(self): + submission_enabled = self._check_submit_validations() + if submission_enabled == self._submission_enabled: + return + self._submission_enabled = submission_enabled + self._event_system.emit( + "submission.enabled.changed", + {"enabled": submission_enabled}, + "controller" + ) + + def _check_submit_validations(self): + if not self._user_values.is_valid: + return False + + if not self.selection_model.project_name: + return False + + if ( + not self._user_values.new_asset_name + and not self.selection_model.asset_id + ): + return False + + return True + + def get_selected_asset_name(self): + project_name = self._selection_model.project_name + asset_id = self._selection_model.asset_id + if not project_name or not asset_id: + return None + asset_item = self._entities_model.get_asset_by_id( + project_name, asset_id + ) + if asset_item: + return asset_item.name + return None def submit(self): + if not self.submission_enabled: + return project_name = self.selection_model.project_name asset_id = self.selection_model.asset_id task_name = self.selection_model.task_name diff --git a/openpype/tools/republisher/republisher_dialog/window.py b/openpype/tools/republisher/republisher_dialog/window.py index 593a95be17..cfc236ad27 100644 --- a/openpype/tools/republisher/republisher_dialog/window.py +++ b/openpype/tools/republisher/republisher_dialog/window.py @@ -1,17 +1,23 @@ +import re import collections from qtpy import QtWidgets, QtGui, QtCore -from openpype.style import load_stylesheet +from openpype.style import load_stylesheet, get_app_icon_path +from openpype.tools.utils import ( + PlaceholderLineEdit, + SeparatorWidget, + get_asset_icon_by_name, + set_style_property, +) -from .control import RepublisherDialogController +from .control import PushToContextController PROJECT_NAME_ROLE = QtCore.Qt.UserRole + 1 ASSET_NAME_ROLE = QtCore.Qt.UserRole + 2 -ASSET_ICON_ROLE = QtCore.Qt.UserRole + 3 -ASSET_ID_ROLE = QtCore.Qt.UserRole + 4 -TASK_NAME_ROLE = QtCore.Qt.UserRole + 5 -TASK_TYPE_ROLE = QtCore.Qt.UserRole + 6 +ASSET_ID_ROLE = QtCore.Qt.UserRole + 3 +TASK_NAME_ROLE = QtCore.Qt.UserRole + 4 +TASK_TYPE_ROLE = QtCore.Qt.UserRole + 5 class ProjectsModel(QtGui.QStandardItemModel): @@ -19,6 +25,8 @@ class ProjectsModel(QtGui.QStandardItemModel): refreshing_text = "< Refreshing >" select_project_text = "< Select Project >" + refreshed = QtCore.Signal() + def __init__(self, controller): super(ProjectsModel, self).__init__() self._controller = controller @@ -59,7 +67,7 @@ class ProjectsModel(QtGui.QStandardItemModel): if project_name is None: continue item = self._items.pop(project_name) - root_item.removeRow(item.row()) + root_item.takeRow(item.row()) for project_name in project_names: if project_name in self._items: @@ -70,6 +78,7 @@ class ProjectsModel(QtGui.QStandardItemModel): if new_items: root_item.appendRows(new_items) + self.refreshed.emit() class ProjectProxyModel(QtCore.QSortFilterProxyModel): @@ -94,6 +103,7 @@ class ProjectProxyModel(QtCore.QSortFilterProxyModel): class AssetsModel(QtGui.QStandardItemModel): + items_changed = QtCore.Signal() empty_text = "< Empty >" def __init__(self, controller): @@ -151,6 +161,7 @@ class AssetsModel(QtGui.QStandardItemModel): self._last_project = project_name self._clear() + self.items_changed.emit() def _on_refresh_start(self, event): pass @@ -165,11 +176,13 @@ class AssetsModel(QtGui.QStandardItemModel): if project_name is None: if None not in self._items: self._clear() + self.items_changed.emit() return asset_items_by_id = self._controller.model.get_assets(project_name) if not asset_items_by_id: self._clear() + self.items_changed.emit() return assets_by_parent_id = collections.defaultdict(list) @@ -201,9 +214,12 @@ class AssetsModel(QtGui.QStandardItemModel): elif item.parent() is not parent_item: new_items.append(item) + icon = get_asset_icon_by_name( + asset_item.icon_name, asset_item.icon_color + ) item.setData(asset_item.name, QtCore.Qt.DisplayRole) + item.setData(icon, QtCore.Qt.DecorationRole) item.setData(asset_item.id, ASSET_ID_ROLE) - item.setData(asset_item.icon, ASSET_ICON_ROLE) hierarchy_queue.append((asset_item.id, item)) @@ -216,10 +232,13 @@ class AssetsModel(QtGui.QStandardItemModel): continue parent = item.parent() if parent is not None: - parent.removeRow(item.row()) + parent.takeRow(item.row()) + + self.items_changed.emit() class TasksModel(QtGui.QStandardItemModel): + items_changed = QtCore.Signal() empty_text = "< Empty >" def __init__(self, controller): @@ -277,6 +296,7 @@ class TasksModel(QtGui.QStandardItemModel): self._last_project = project_name self._clear() + self.items_changed.emit() def _on_asset_refresh_finish(self, event): self._refresh(event["project_name"]) @@ -293,6 +313,7 @@ class TasksModel(QtGui.QStandardItemModel): if project_name is None: if None not in self._items: self._clear() + self.items_changed.emit() return asset_id = self._controller.selection_model.asset_id @@ -301,6 +322,7 @@ class TasksModel(QtGui.QStandardItemModel): ) if not task_items: self._clear() + self.items_changed.emit() return root_item = self.invisibleRootItem() @@ -338,74 +360,153 @@ class TasksModel(QtGui.QStandardItemModel): if parent is not None: parent.removeRow(item.row()) + self.items_changed.emit() -class RepublisherDialogWindow(QtWidgets.QWidget): + +class PushToContextSelectWindow(QtWidgets.QWidget): def __init__(self, controller=None): - super(RepublisherDialogWindow, self).__init__() + super(PushToContextSelectWindow, self).__init__() if controller is None: - controller = RepublisherDialogController() + controller = PushToContextController() self._controller = controller - main_splitter = QtWidgets.QSplitter(self) + self.setWindowTitle("Push to project (select context)") + self.setWindowIcon(QtGui.QIcon(get_app_icon_path())) - left_widget = QtWidgets.QWidget(main_splitter) + header_widget = QtWidgets.QWidget(self) - project_combobox = QtWidgets.QComboBox(left_widget) + header_label = QtWidgets.QLabel(controller.src_label, header_widget) + + header_layout = QtWidgets.QHBoxLayout(header_widget) + header_layout.setContentsMargins(0, 0, 0, 0) + header_layout.addWidget(header_label) + + main_splitter = QtWidgets.QSplitter(QtCore.Qt.Horizontal, self) + + context_widget = QtWidgets.QWidget(main_splitter) + + project_combobox = QtWidgets.QComboBox(context_widget) project_model = ProjectsModel(controller) project_proxy = ProjectProxyModel() project_proxy.setSourceModel(project_model) + project_proxy.setDynamicSortFilter(True) project_delegate = QtWidgets.QStyledItemDelegate() project_combobox.setItemDelegate(project_delegate) project_combobox.setModel(project_proxy) - asset_view = QtWidgets.QTreeView(left_widget) + asset_task_splitter = QtWidgets.QSplitter( + QtCore.Qt.Vertical, context_widget + ) + + asset_view = QtWidgets.QTreeView(asset_task_splitter) asset_view.setHeaderHidden(True) asset_model = AssetsModel(controller) - asset_view.setModel(asset_model) + asset_proxy = QtCore.QSortFilterProxyModel() + asset_proxy.setSourceModel(asset_model) + asset_proxy.setDynamicSortFilter(True) + asset_view.setModel(asset_proxy) - left_layout = QtWidgets.QVBoxLayout(left_widget) - left_layout.setContentsMargins(0, 0, 0, 0) - left_layout.addWidget(project_combobox, 0) - left_layout.addWidget(asset_view, 1) - - right_widget = QtWidgets.QWidget(main_splitter) - - task_view = QtWidgets.QListView(right_widget) + task_view = QtWidgets.QListView(asset_task_splitter) task_proxy = QtCore.QSortFilterProxyModel() task_model = TasksModel(controller) task_proxy.setSourceModel(task_model) + task_proxy.setDynamicSortFilter(True) task_view.setModel(task_proxy) - right_layout = QtWidgets.QVBoxLayout(right_widget) - right_layout.setContentsMargins(0, 0, 0, 0) - right_layout.addWidget(task_view, 1) + asset_task_splitter.addWidget(asset_view) + asset_task_splitter.addWidget(task_view) - main_splitter.addWidget(left_widget) - main_splitter.addWidget(right_widget) + context_layout = QtWidgets.QVBoxLayout(context_widget) + context_layout.setContentsMargins(0, 0, 0, 0) + context_layout.addWidget(project_combobox, 0) + context_layout.addWidget(asset_task_splitter, 1) + # --- Inputs widget --- + inputs_widget = QtWidgets.QWidget(main_splitter) + + asset_name_input = PlaceholderLineEdit(inputs_widget) + asset_name_input.setPlaceholderText("< Name of new asset >") + asset_name_input.setObjectName("ValidatedLineEdit") + + variant_input = PlaceholderLineEdit(inputs_widget) + variant_input.setPlaceholderText("< Variant >") + variant_input.setObjectName("ValidatedLineEdit") + + comment_input = PlaceholderLineEdit(inputs_widget) + comment_input.setPlaceholderText("< Publish comment >") + + inputs_layout = QtWidgets.QFormLayout(inputs_widget) + inputs_layout.setContentsMargins(0, 0, 0, 0) + inputs_layout.addRow("New asset name", asset_name_input) + inputs_layout.addRow("Variant", variant_input) + inputs_layout.addRow("Comment", comment_input) + + main_splitter.addWidget(context_widget) + main_splitter.addWidget(inputs_widget) + + # --- Buttons widget --- btns_widget = QtWidgets.QWidget(self) - close_btn = QtWidgets.QPushButton("Close", btns_widget) - select_btn = QtWidgets.QPushButton("Select", btns_widget) + cancel_btn = QtWidgets.QPushButton("Cancel", btns_widget) + publish_btn = QtWidgets.QPushButton("Publish", btns_widget) btns_layout = QtWidgets.QHBoxLayout(btns_widget) btns_layout.setContentsMargins(0, 0, 0, 0) btns_layout.addStretch(1) - btns_layout.addWidget(close_btn, 0) - btns_layout.addWidget(select_btn, 0) + btns_layout.addWidget(cancel_btn, 0) + btns_layout.addWidget(publish_btn, 0) - main_layout = QtWidgets.QHBoxLayout(self) + sep_1 = SeparatorWidget(parent=self) + sep_2 = SeparatorWidget(parent=self) + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.addWidget(header_widget, 0) + main_layout.addWidget(sep_1, 0) main_layout.addWidget(main_splitter, 1) + main_layout.addWidget(sep_2, 0) main_layout.addWidget(btns_widget, 0) + show_timer = QtCore.QTimer() + show_timer.setInterval(1) + + user_input_changed_timer = QtCore.QTimer() + user_input_changed_timer.setInterval(200) + user_input_changed_timer.setSingleShot(True) + + show_timer.timeout.connect(self._on_show_timer) + user_input_changed_timer.timeout.connect(self._on_user_input_timer) + asset_name_input.textChanged.connect(self._on_new_asset_change) + variant_input.textChanged.connect(self._on_variant_change) + comment_input.textChanged.connect(self._on_comment_change) + project_model.refreshed.connect(self._on_projects_refresh) project_combobox.currentIndexChanged.connect(self._on_project_change) asset_view.selectionModel().selectionChanged.connect( self._on_asset_change ) + asset_model.items_changed.connect(self._on_asset_model_change) task_view.selectionModel().selectionChanged.connect( self._on_task_change ) - select_btn.clicked.connect(self._on_select_click) - close_btn.clicked.connect(self._on_close_click) + task_model.items_changed.connect(self._on_task_model_change) + publish_btn.clicked.connect(self._on_select_click) + cancel_btn.clicked.connect(self._on_close_click) + + controller.event_system.add_callback( + "new_asset_name.changed", self._on_controller_new_asset_change + ) + controller.event_system.add_callback( + "variant.changed", self._on_controller_variant_change + ) + controller.event_system.add_callback( + "comment.changed", self._on_controller_comment_change + ) + controller.event_system.add_callback( + "submission.enabled.changed", self._on_submission_change + ) + controller.event_system.add_callback( + "source.changed", self._on_controller_source_change + ) + + self._header_label = header_label + self._main_splitter = main_splitter self._project_combobox = project_combobox self._project_model = project_model @@ -414,17 +515,153 @@ class RepublisherDialogWindow(QtWidgets.QWidget): self._asset_view = asset_view self._asset_model = asset_model + self._asset_proxy_model = asset_proxy self._task_view = task_view + self._task_proxy_model = task_proxy + self._variant_input = variant_input + self._asset_name_input = asset_name_input + self._comment_input = comment_input + + self._publish_btn = publish_btn + + self._user_input_changed_timer = user_input_changed_timer + # Store current value on input text change + # The value is unset when is passed to controller + # The goal is to have controll over changes happened during user change + # in UI and controller auto-changes + self._variant_input_text = None + self._new_asset_name_input_text = None + self._comment_input_text = None + self._show_timer = show_timer + self._show_counter = 2 self._first_show = True + publish_btn.setEnabled(False) + + if controller.user_values.new_asset_name: + asset_name_input.setText(controller.user_values.new_asset_name) + if controller.user_values.variant: + variant_input.setText(controller.user_values.variant) + self._invalidate_variant() + self._invalidate_new_asset_name() + + @property + def controller(self): + return self._controller + def showEvent(self, event): - super(RepublisherDialogWindow, self).showEvent(event) + super(PushToContextSelectWindow, self).showEvent(event) if self._first_show: self._first_show = False - self._controller.model.refresh_projects() self.setStyleSheet(load_stylesheet()) + self._invalidate_variant() + self._show_timer.start() + + def _on_show_timer(self): + if self._show_counter == 0: + self._show_timer.stop() + return + + self._show_counter -= 1 + if self._show_counter == 1: + width = 740 + height = 640 + inputs_width = 360 + self.resize(width, height) + self._main_splitter.setSizes([width - inputs_width, inputs_width]) + + if self._show_counter > 0: + return + + self._controller.model.refresh_projects() + + def _on_new_asset_change(self, text): + self._new_asset_name_input_text = text + self._user_input_changed_timer.start() + + def _on_variant_change(self, text): + self._variant_input_text = text + self._user_input_changed_timer.start() + + def _on_comment_change(self, text): + self._comment_input_text = text + self._user_input_changed_timer.start() + + def _on_user_input_timer(self): + asset_name = self._new_asset_name_input_text + if asset_name is not None: + self._new_asset_name_input_text = None + self._controller.user_values.set_new_asset(asset_name) + + variant = self._variant_input_text + if variant is not None: + self._variant_input_text = None + self._controller.user_values.set_variant(variant) + + comment = self._comment_input_text + if comment is not None: + self._comment_input_text = None + self._controller.user_values.set_comment(comment) + + def _on_controller_new_asset_change(self, event): + asset_name = event["changes"]["new_asset_name"]["new"] + if ( + self._new_asset_name_input_text is None + and asset_name != self._asset_name_input.text() + ): + self._asset_name_input.setText(asset_name) + + self._invalidate_new_asset_name() + + def _on_controller_variant_change(self, event): + is_valid_changes = event["changes"]["is_valid"] + variant = event["changes"]["variant"]["new"] + if ( + self._variant_input_text is None + and variant != self._variant_input.text() + ): + self._variant_input.setText(variant) + + if is_valid_changes["old"] != is_valid_changes["new"]: + self._invalidate_variant() + + def _on_controller_comment_change(self, event): + comment = event["comment"] + if ( + self._comment_input_text is None + and comment != self._comment_input.text() + ): + self._comment_input.setText(comment) + + def _on_controller_source_change(self): + self._header_label.setText(self._controller.src_label) + + def _invalidate_new_asset_name(self): + asset_name = self._controller.user_values.new_asset_name + self._task_view.setVisible(not asset_name) + + valid = None + if asset_name: + valid = self._controller.user_values.is_new_asset_name_valid + + state = "" + if valid is True: + state = "valid" + elif valid is False: + state = "invalid" + set_style_property(self._asset_name_input, "state", state) + + def _invalidate_variant(self): + valid = self._controller.user_values.is_variant_valid + state = "invalid" + if valid is True: + state = "valid" + set_style_property(self._variant_input, "state", state) + + def _on_projects_refresh(self): + self._project_proxy.sort(0, QtCore.Qt.AscendingOrder) def _on_project_change(self): idx = self._project_combobox.currentIndex() @@ -445,6 +682,12 @@ class RepublisherDialogWindow(QtWidgets.QWidget): asset_id = model.data(index, ASSET_ID_ROLE) self._controller.selection_model.select_asset(asset_id) + def _on_asset_model_change(self): + self._asset_proxy_model.sort(0, QtCore.Qt.AscendingOrder) + + def _on_task_model_change(self): + self._task_proxy_model.sort(0, QtCore.Qt.AscendingOrder) + def _on_task_change(self): indexes = self._task_view.selectedIndexes() index = next(iter(indexes), None) @@ -454,6 +697,9 @@ class RepublisherDialogWindow(QtWidgets.QWidget): task_name = model.data(index, TASK_NAME_ROLE) self._controller.selection_model.select_task(task_name) + def _on_submission_change(self, event): + self._publish_btn.setEnabled(event["enabled"]) + def _on_close_click(self): self.close() @@ -464,14 +710,29 @@ class RepublisherDialogWindow(QtWidgets.QWidget): def main(): app = QtWidgets.QApplication.instance() if not app: + # 'AA_EnableHighDpiScaling' must be set before app instance creation + high_dpi_scale_attr = getattr( + QtCore.Qt, "AA_EnableHighDpiScaling", None + ) + if high_dpi_scale_attr is not None: + QtWidgets.QApplication.setAttribute(high_dpi_scale_attr) + app = QtWidgets.QApplication([]) + for attr_name in ( + "AA_UseHighDpiPixmaps", + ): + attr = getattr(QtCore.Qt, attr_name, None) + if attr is not None: + app.setAttribute(attr) + # TODO find way how to get these project_name = None - representation_id = None + version_id = None # Show window dialog - window = RepublisherDialogWindow() + window = PushToContextSelectWindow() + window.controller.set_source(project_name, version_id) window.show() app.exec_() From f1d7ce1d11b683984a322bed25848d70da5e4a31 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 5 Jan 2023 10:22:05 +0100 Subject: [PATCH 06/24] modified processing logic of republishing --- .../republisher_processor/control.py | 815 +++++++++++++----- 1 file changed, 621 insertions(+), 194 deletions(-) diff --git a/openpype/tools/republisher/republisher_processor/control.py b/openpype/tools/republisher/republisher_processor/control.py index 8a235561d7..36e78bfdc6 100644 --- a/openpype/tools/republisher/republisher_processor/control.py +++ b/openpype/tools/republisher/republisher_processor/control.py @@ -1,43 +1,63 @@ import os import re import copy -import requests +import socket +import itertools + from openpype.client import ( get_project, get_asset_by_id, + get_subset_by_id, get_subset_by_name, - get_representation_by_id, - get_representation_parents, + get_version_by_id, + get_last_version_by_subset_id, + get_version_by_name, + get_representations, + get_representation_by_name, +) +from openpype.client.operations import ( + OperationsSession, + new_subset_document, + new_version_doc, + prepare_version_update_data, +) +from openpype.lib import ( + StringTemplate, + get_openpype_username, + get_formatted_current_time, ) -from openpype.lib import StringTemplate from openpype.settings import get_project_settings +from openpype.pipeline import Anatomy +from openpype.pipeline.template_data import get_template_data from openpype.pipeline.publish import get_publish_template_name from openpype.pipeline.create import get_subset_name +UNKNOWN = object() + class RepublishError(Exception): pass -class RepublishItem: +class ProjectPushItem: def __init__( self, src_project_name, - src_representation_id, + src_version_id, dst_project_name, dst_asset_id, dst_task_name, dst_version=None ): self.src_project_name = src_project_name - self.src_representation_id = src_representation_id + self.src_version_id = src_version_id self.dst_project_name = dst_project_name self.dst_asset_id = dst_asset_id self.dst_task_name = dst_task_name self.dst_version = dst_version self._id = "|".join([ src_project_name, - src_representation_id, + src_version_id, dst_project_name, dst_asset_id, dst_task_name @@ -50,14 +70,391 @@ class RepublishItem: def __repr__(self): return "{} - {} -> {}/{}/{}".format( self.src_project_name, - self.src_representation_id, + self.src_version_id, self.dst_project_name, self.dst_asset_id, self.dst_task_name ) -class RepublishItemStatus: +class ProjectPushRepreItem: + """Representation item. + + Args: + repre_doc (Dict[str, Ant]): Representation document. + roots (Dict[str, str]): Project roots (based on project anatomy). + """ + + def __init__(self, repre_doc, roots): + self._repre_doc = repre_doc + self._roots = roots + self._src_files = None + self._resource_files = None + + @property + def repre_doc(self): + return self._repre_doc + + @property + def src_files(self): + if self._src_files is None: + self.get_source_files() + return self._src_files + + @property + def resource_files(self): + if self._resource_files is None: + self.get_source_files() + return self._resource_files + + @staticmethod + def validate_source_files(src_files, resource_files): + if not src_files: + raise AssertionError(( + "Couldn't figure out source files from representation." + " Found resource files {}" + ).format(", ".join(str(i) for i in resource_files))) + + invalid_items = [ + item + for item in itertools.chain(src_files, resource_files) + if not item.is_valid_file + ] + if invalid_items: + raise AssertionError(( + "Source files that were not found on disk: {}" + ).format(", ".join(str(i) for i in invalid_items))) + + def get_source_files(self): + if self._src_files is not None: + return self._src_files, self._resource_files + + repre_context = self._repre_doc["context"] + if "frame" in repre_context or "udim" in repre_context: + src_files, resource_files = self._get_source_files_with_frames() + else: + src_files, resource_files = self._get_source_files() + + self.validate_source_files(src_files, resource_files) + + self._src_files = src_files + self._resource_files = resource_files + return self._src_files, self._resource_files + + def _get_source_files_with_frames(self): + frame_placeholder = "__frame__" + udim_placeholder = "__udim__" + src_files = [] + resource_files = [] + template = self._repre_doc["data"]["template"] + repre_context = self._repre_doc["context"] + fill_repre_context = copy.deepcopy(repre_context) + if "frame" in fill_repre_context: + fill_repre_context["frame"] = frame_placeholder + + if "udim" in fill_repre_context: + fill_repre_context["udim"] = udim_placeholder + + fill_roots = fill_repre_context["root"] + for root_name in tuple(fill_roots.keys()): + fill_roots[root_name] = "{{root[{}]}}".format(root_name) + repre_path = StringTemplate.format_template(template, + fill_repre_context) + repre_path = repre_path.replace("\\", "/") + src_dirpath, src_basename = os.path.split(repre_path) + src_basename = ( + re.escape(src_basename) + .replace(frame_placeholder, "(?P[0-9]+)") + .replace(udim_placeholder, "(?P[0-9]+)") + ) + src_basename_regex = re.compile("^{}$".format(src_basename)) + for file_info in self._repre_doc["files"]: + filepath_template = file_info["path"].replace("\\", "/") + filepath = filepath_template.format(root=self._roots) + dirpath, basename = os.path.split(filepath_template) + if dirpath != src_dirpath or not src_basename_regex.match(basename): + relative_dir = dirpath.replace(src_dirpath, "") + if relative_dir: + relative_path = "/".join([relative_dir, basename]) + else: + relative_path = basename + resource_files.append(ResourceFile(filepath, relative_path)) + continue + + frame = None + udim = None + for item in src_basename_regex.finditer(basename): + group_name = item.lastgroup + value = item.group(group_name) + if group_name == "frame": + frame = int(value) + elif group_name == "udim": + udim = value + + src_files.append(SourceFile(filepath, frame, udim)) + + return src_files, resource_files + + def _get_source_files(self): + src_files = [] + resource_files = [] + template = self._repre_doc["data"]["template"] + repre_context = self._repre_doc["context"] + fill_repre_context = copy.deepcopy(repre_context) + fill_roots = fill_repre_context["root"] + for root_name in tuple(fill_roots.keys()): + fill_roots[root_name] = "{{root[{}]}}".format(root_name) + repre_path = StringTemplate.format_template(template, + fill_repre_context) + repre_path = repre_path.replace("\\", "/") + src_dirpath = os.path.dirname(repre_path) + for file_info in self._repre_doc["files"]: + filepath_template = file_info["path"].replace("\\", "/") + filepath = filepath_template.format(root=self._roots) + if filepath_template == repre_path: + src_files.append(SourceFile(filepath)) + else: + dirpath, basename = os.path.split(filepath_template) + relative_dir = dirpath.replace(src_dirpath, "") + if relative_dir: + relative_path = "/".join([relative_dir, basename]) + else: + relative_path = basename + + resource_files.append( + ResourceFile(filepath, relative_path) + ) + return src_files, resource_files + + +class ProjectPushItemProcess: + """ + Args: + item (ProjectPushItem): Item which is being processed. + """ + + # TODO how to define 'variant'? - ask user + variant = "Main" + # TODO where to get host?!!! + host_name = "republisher" + + def __init__(self, item): + self._item = item + self._src_project_doc = UNKNOWN + self._src_asset_doc = UNKNOWN + self._src_subset_doc = UNKNOWN + self._src_version_doc = UNKNOWN + self._src_repre_items = UNKNOWN + self._src_anatomy = None + + self._project_doc = UNKNOWN + self._anatomy = None + self._asset_doc = UNKNOWN + self._task_info = UNKNOWN + self._subset_doc = None + self._version_doc = None + + self._family = UNKNOWN + self._subset_name = UNKNOWN + + self._project_settings = UNKNOWN + self._template_name = UNKNOWN + + self._src_files = UNKNOWN + self._src_resource_files = UNKNOWN + + def get_src_project_doc(self): + if self._src_project_doc is UNKNOWN: + self._src_project_doc = get_project(self._item.src_project_name) + return self._src_project_doc + + def get_src_anatomy(self): + if self._src_anatomy is None: + self._src_anatomy = Anatomy(self._item.src_project_name) + return self._src_anatomy + + def get_src_asset_doc(self): + if self._src_asset_doc is UNKNOWN: + asset_doc = None + subset_doc = self.get_src_subset_doc() + if subset_doc: + asset_doc = get_asset_by_id( + self._item.src_project_name, + subset_doc["parent"] + ) + self._src_asset_doc = asset_doc + return self._src_asset_doc + + def get_src_subset_doc(self): + if self._src_subset_doc is UNKNOWN: + version_doc = self.get_src_version_doc() + subset_doc = None + if version_doc: + subset_doc = get_subset_by_id( + self._item.src_project_name, + version_doc["parent"] + ) + self._src_subset_doc = subset_doc + return self._src_subset_doc + + def get_src_version_doc(self): + if self._src_version_doc is UNKNOWN: + self._src_version_doc = get_version_by_id( + self._item.src_project_name, self._item.src_version_id + ) + return self._src_version_doc + + def get_src_repre_items(self): + if self._src_repre_items is UNKNOWN: + repre_items = None + version_doc = self.get_src_version_doc() + if version_doc: + repre_docs = get_representations( + self._item.src_project_name, + version_ids=[version_doc["_id"]] + ) + repre_items = [ + ProjectPushRepreItem(repre_doc, self.src_anatomy.roots) + for repre_doc in repre_docs + ] + self._src_repre_items = repre_items + return self._src_repre_items + + src_project_doc = property(get_src_project_doc) + src_anatomy = property(get_src_anatomy) + src_asset_doc = property(get_src_asset_doc) + src_subset_doc = property(get_src_subset_doc) + src_version_doc = property(get_src_version_doc) + src_repre_items = property(get_src_repre_items) + + def get_project_doc(self): + if self._project_doc is UNKNOWN: + self._project_doc = get_project(self._item.dst_project_name) + return self._project_doc + + def get_anatomy(self): + if self._anatomy is None: + self._anatomy = Anatomy(self._item.dst_project_name) + return self._anatomy + + def get_asset_doc(self): + if self._asset_doc is UNKNOWN: + self._asset_doc = get_asset_by_id( + self._item.dst_project_name, self._item.dst_asset_id + ) + return self._asset_doc + + def get_task_info(self): + if self._task_info is UNKNOWN: + task_name = self._item.dst_task_name + if not task_name: + self._task_info = {} + return self._task_info + + project_doc = self.get_project_doc() + asset_doc = self.get_asset_doc() + if not project_doc or not asset_doc: + self._task_info = None + return self._task_info + + asset_tasks = asset_doc.get("data", {}).get("tasks") or {} + task_info = asset_tasks.get(task_name) + if not task_info: + self._task_info = None + return self._task_info + + # Create copy of task info to avoid changing data in asset document + task_info = copy.deepcopy(task_info) + task_info["name"] = task_name + # Fill rest of task information based on task type + task_type = task_info["type"] + task_type_info = project_doc["config"]["tasks"].get(task_type, {}) + task_info.update(task_type_info) + self._task_info = task_info + + return self._task_info + + def get_subset_doc(self): + return self._subset_doc + + def set_subset_doc(self, subset_doc): + self._subset_doc = subset_doc + + def get_version_doc(self): + return self._version_doc + + def set_version_doc(self, version_doc): + self._version_doc = version_doc + + project_doc = property(get_project_doc) + anatomy = property(get_anatomy) + asset_doc = property(get_asset_doc) + task_info = property(get_task_info) + subset_doc = property(get_subset_doc) + version_doc = property(get_version_doc, set_version_doc) + + def get_project_settings(self): + if self._project_settings is UNKNOWN: + self._project_settings = get_project_settings( + self._item.dst_project_name + ) + return self._project_settings + + project_settings = property(get_project_settings) + + @property + def family(self): + if self._family is UNKNOWN: + family = None + subset_doc = self.src_subset_doc + if subset_doc: + family = subset_doc["data"].get("family") + families = subset_doc["data"].get("families") + if not family and families: + family = families[0] + self._family = family + return self._family + + @property + def subset_name(self): + if self._subset_name is UNKNOWN: + subset_name = None + family = self.family + asset_doc = self.asset_doc + task_info = self.task_info + if family and asset_doc and task_info: + subset_name = get_subset_name( + family, + self.variant, + task_info["name"], + asset_doc, + project_name=self._item.dst_project_name, + host_name=self.host_name, + project_settings=self.project_settings + ) + self._subset_name = subset_name + return self._subset_name + + @property + def template_name(self): + if self._template_name is UNKNOWN: + task_info = self.task_info + family = self.family + template_name = None + if family and task_info: + template_name = get_publish_template_name( + self._item.dst_project_name, + self.host_name, + self.family, + task_info["name"], + task_info["type"], + project_settings=self.project_settings + ) + self._template_name = template_name + return self._template_name + + +class ProjectPushItemStatus: def __init__( self, item, @@ -131,9 +528,18 @@ class RepublisherController: return dict(self._items) -class SourceFile: - def __init__(self, path, frame=None, udim=None): +class FileItem(object): + def __init__(self, path): self.path = path + + @property + def is_valid_file(self): + return os.path.exists(self.path) and os.path.isfile(self.path) + + +class SourceFile(FileItem): + def __init__(self, path, frame=None, udim=None): + super(SourceFile, self).__init__(path) self.frame = frame self.udim = udim @@ -147,189 +553,231 @@ class SourceFile: return "<{}> '{}'".format(" - ".join(subparts), self.path) -class ResourceFile: +class ResourceFile(FileItem): def __init__(self, path, relative_path): - self.path = path + super(ResourceFile, self).__init__(path) self.relative_path = relative_path def __repr__(self): return "<{}> '{}'".format(self.__class__.__name__, self.relative_path) -def get_source_files_with_frames(src_representation): - frame_placeholder = "__frame__" - udim_placeholder = "__udim__" - src_files = [] - resource_files = [] - template = src_representation["data"]["template"] - repre_context = src_representation["context"] - fill_repre_context = copy.deepcopy(repre_context) - if "frame" in fill_repre_context: - fill_repre_context["frame"] = frame_placeholder +def _make_sure_subset_exists(item_process, project_name, operations): + dst_asset_doc = item_process.asset_doc + subset_name = item_process.subset_name + family = item_process.family + asset_id = dst_asset_doc["_id"] + subset_doc = get_subset_by_name(project_name, subset_name, asset_id) + if subset_doc: + return subset_doc - if "udim" in fill_repre_context: - fill_repre_context["udim"] = udim_placeholder - - fill_roots = fill_repre_context["root"] - for root_name in tuple(fill_roots.keys()): - fill_roots[root_name] = "{{root[{}]}}".format(root_name) - repre_path = StringTemplate.format_template(template, fill_repre_context) - repre_path = repre_path.replace("\\", "/") - src_dirpath, src_basename = os.path.split(repre_path) - src_basename = ( - re.escape(src_basename) - .replace(frame_placeholder, "(?P[0-9]+)") - .replace(udim_placeholder, "(?P[0-9]+)") + data = { + "families": [family] + } + subset_doc = new_subset_document( + subset_name, family, asset_id, data ) - src_basename_regex = re.compile("^{}$".format(src_basename)) - for file_info in src_representation["files"]: - filepath = file_info["path"].replace("\\", "/") - dirpath, basename = os.path.split(filepath) - if dirpath != src_dirpath or not src_basename_regex.match(basename): - relative_dir = dirpath.replace(src_dirpath, "") - if relative_dir: - relative_path = "/".join([relative_dir, basename]) - else: - relative_path = basename - resource_files.append(ResourceFile(filepath, relative_path)) - continue - - frame = None - udim = None - for item in src_basename_regex.finditer(basename): - group_name = item.lastgroup - value = item.group(group_name) - if group_name == "frame": - frame = int(value) - elif group_name == "udim": - udim = value - - src_files.append(SourceFile(filepath, frame, udim)) - - return src_files, resource_files + operations.create_entity(project_name, "subset", subset_doc) + item_process.set_subset_doc(subset_doc) -def get_source_files(src_representation): - repre_context = src_representation["context"] - if "frame" in repre_context or "udim" in repre_context: - return get_source_files_with_frames(src_representation) - - src_files = [] - resource_files = [] - template = src_representation["data"]["template"] - fill_repre_context = copy.deepcopy(repre_context) - fill_roots = fill_repre_context["root"] - for root_name in tuple(fill_roots.keys()): - fill_roots[root_name] = "{{root[{}]}}".format(root_name) - repre_path = StringTemplate.format_template(template, fill_repre_context) - repre_path = repre_path.replace("\\", "/") - src_dirpath = os.path.dirname(repre_path) - for file_info in src_representation["files"]: - filepath = file_info["path"] - if filepath == repre_path: - src_files.append(SourceFile(filepath)) - else: - dirpath, basename = os.path.split(filepath) - relative_dir = dirpath.replace(src_dirpath, "") - if relative_dir: - relative_path = "/".join([relative_dir, basename]) - else: - relative_path = basename - resource_files.append(ResourceFile(filepath, relative_path)) - return src_files, resource_files - - -def _republish_to( - item, +def _make_sure_version_exists( item_process, - src_representation, - src_representation_parents, - dst_asset_doc, - dst_task_info + project_name, + version, + operations ): + """Make sure version document exits in database. + + Args: + item_process (ProjectPushItemProcess): Item handling process. + project_name (str): Name of project where version should live. + version (Union[int, None]): Number of version. Latest is used when + 'None' is passed. + operations (OperationsSession): Session which handler creation and + update of entities. + + Returns: + Tuple[Dict[str, Any], bool]: New version document and boolean if version + already existed in database. + """ + + src_version_doc = item_process.src_version_doc + subset_doc = item_process.subset_doc + subset_id = subset_doc["_id"] + src_data = src_version_doc["data"] + families = subset_doc["data"].get("families") + if not families: + families = [subset_doc["data"]["family"]] + + version_data = { + "families": list(families), + "fps": src_data.get("fps"), + "source": src_data.get("source"), + "machine": socket.gethostname(), + "comment": "", + "author": get_openpype_username(), + "time": get_formatted_current_time(), + } + if version is None: + last_version_doc = get_last_version_by_subset_id( + project_name, subset_id + ) + version = 1 + if last_version_doc: + version += int(last_version_doc["name"]) + + existing_version_doc = get_version_by_name( + project_name, version, subset_id + ) + # Update existing version + if existing_version_doc: + version_doc = new_version_doc( + version, subset_id, version_data, existing_version_doc["_id"] + ) + update_data = prepare_version_update_data( + existing_version_doc, version_doc + ) + if update_data: + operations.update_entity( + project_name, + "version", + existing_version_doc["_id"], + update_data + ) + item_process.set_version_doc(version_doc) + + return + + if version is None: + last_version_doc = get_last_version_by_subset_id( + project_name, subset_id + ) + version = 1 + if last_version_doc: + version += int(last_version_doc["name"]) + + version_doc = new_version_doc( + version, subset_id, version_data + ) + operations.create_entity(project_name, "version", version_doc) + + item_process.set_version_doc(version_doc) + + +def _integrate_representations(item, item_process, item_status, operations): """ Args: - item (RepublishItem): Item to process. - item_process (RepublishItemStatus): Item process information. - src_representation (Dict[str, Any]): Representation document. - src_representation_parents (Tuple[Any, Any, Any, Any]): Representation - parent documents. - dst_asset_doc (Dict[str, Any]): Asset document as destination of - publishing. - dst_task_info (Dict[str, str]): Task information with prepared - infromation from project config. + item (ProjectPushItem): Item to be pushed to different project. + item_process (ProjectPushItemProcess): Process of push item. """ - src_subset_doc = src_representation_parents[1] - family = src_subset_doc["data"].get("family") - if not family: - families = src_subset_doc["data"]["families"] - family = families[0] + version_id = item_process.version_doc["_id"] + repre_names = { + repre_item.repre_doc["name"] + for repre_item in item_process.src_repre_items + } + existing_repres = get_representations( + item.dst_project_name, + representation_names=repre_names, + version_ids=[version_id] + ) + existing_repres_by_name = { + repre_doc["name"] : repre_doc + for repre_doc in existing_repres + } + anatomy = item_process.anatomy + formatting_data = get_template_data( + item_process.project_doc, + item_process.asset_doc, + item.dst_task_name, + item_process.host_name + ) - item_process.add_progress_message( + +def _republish_to(item, item_process, item_status): + """ + + Args: + item (ProjectPushItem): Item to process. + item_process (ProjectPushItemProcess): Item process information. + item_status (ProjectPushItemStatus): Item status information. + """ + + family = item_process.family + item_status.add_progress_message( f"Republishing family '{family}' (Based on source subset)" ) - # TODO how to define 'variant'? - variant = "Main" - # TODO where to get host? - host_name = "republisher" - project_settings = get_project_settings(item.dst_project_name) - subset_name = get_subset_name( - family, - variant, - dst_task_info["name"], - dst_asset_doc, - project_name=item.dst_project_name, - host_name=host_name, - project_settings=project_settings - ) - item_process.add_progress_message(f"Final subset name is '{subset_name}'") + subset_name = item_process.subset_name + item_status.add_progress_message(f"Final subset name is '{subset_name}'") - template_name = get_publish_template_name( - item.dst_project_name, - host_name, - family, - dst_task_info["name"], - dst_task_info["type"], - project_settings=project_settings - ) - item_process.add_progress_message( + template_name = item_process.template_name + item_status.add_progress_message( f"Using template '{template_name}' for integration" ) - src_files, resource_files = get_source_files(src_representation) + repre_items = item_process.src_repre_items + file_count = sum( + len(repre_item.src_files) + len(repre_item.resource_files) + for repre_item in repre_items + ) + item_status.add_progress_message( + f"Representation has {file_count} files to integrate" + ) + + operations = OperationsSession() + item_status.add_progress_message( + f"Integration to {item.dst_project_name} begins." + ) + _make_sure_subset_exists( + item_process, + item.dst_project_name, + operations + ) + _make_sure_version_exists( + item_process, + item.dst_project_name, + item.dst_version, + operations + ) + _integrate_representations(item, item_process, item_status, operations) -def _process_item(item, item_process): +def _process_item(item, item_process, item_status): + """ + + Args: + item (ProjectPushItem): Item defying the source and destination. + item_process (ProjectPushItemProcess): Process item. + item_status (ProjectPushItemStatus): Status of process item. + """ + # Query all entities source and destination # - all of them are required for processing to exist # --- Source entities --- # Project - we just need validation of existence src_project_name = item.src_project_name - src_project_doc = get_project(src_project_name, fields=["name"]) + src_project_doc = item_process.get_src_project_doc() if not src_project_doc: - item_process.error = ( + item_status.error = ( f"Source project '{src_project_name}' was not found" ) return - item_process.add_progress_message(f"Project '{src_project_name}' found") + item_status.add_progress_message(f"Project '{src_project_name}' found") # Representation - contains information of source files and template data - src_representation_id = item.src_representation_id - src_representation = get_representation_by_id( - src_project_name, src_representation_id - ) - if not src_representation: - item_process.error = ( - f"Representation with id '{src_representation_id}'" - f" was not found in project '{src_project_name}'" + repre_items = item_process.get_src_repre_items() + if not repre_items: + item_status.error = ( + f"Version {item.src_version_id} does not have any representations" ) return - item_process.add_progress_message( - f"Representation with id '{src_representation_id}' found" - f" in project '{src_project_name}'" + + item_status.add_progress_message( + f"Found {len(repre_items)} representations on" + f" version {item.src_version_id} in project '{src_project_name}'" ) # --- Destination entities --- @@ -338,73 +786,51 @@ def _process_item(item, item_process): dst_task_name = item.dst_task_name # Validate project existence - dst_project_doc = get_project(dst_project_name, fields=["name", "config"]) + dst_project_doc = item_process.get_project_doc() if not dst_project_doc: - item_process.error = ( + item_status.error = ( f"Destination project '{dst_project_name}' was not found" ) return - item_process.add_progress_message(f"Project '{dst_project_name}' found") + item_status.add_progress_message(f"Project '{dst_project_name}' found") # Get asset document - dst_asset_doc = get_asset_by_id( - dst_project_name, - dst_asset_id - ) - if not dst_asset_doc: - item_process.error = ( + if not item_process.asset_doc: + item_status.error = ( f"Destination asset with id '{dst_asset_id}'" f" was not found in project '{dst_project_name}'" ) return - item_process.add_progress_message(( + item_status.add_progress_message(( f"Asset with id '{dst_asset_id}'" f" found in project '{dst_project_name}'" )) # Get task information from asset document - asset_tasks = dst_asset_doc.get("data", {}).get("tasks") or {} - task_info = asset_tasks.get(dst_task_name) - if not task_info: - item_process.error = ( + if not item_process.task_info: + item_status.error = ( f"Destination task '{dst_task_name}'" f" was not found on asset with id '{dst_asset_id}'" f" in project '{dst_project_name}'" ) return - item_process.add_progress_message(( + item_status.add_progress_message(( f"Task with name '{dst_task_name}'" f" found on asset with id '{dst_asset_id}'" f" in project '{dst_project_name}'" )) - # Create copy of task info to avoid changing data in asset document - dst_task_info = copy.deepcopy(task_info) - dst_task_info["name"] = dst_task_name - # Fill rest of task information based on task type - task_type = dst_task_info["type"] - task_type_info = dst_project_doc["config"]["tasks"].get(task_type) - dst_task_info.update(task_type_info) - src_representation_parents = get_representation_parents( - src_project_name, src_representation - ) - _republish_to( - item, - item_process, - src_representation, - src_representation_parents, - dst_asset_doc, - dst_task_info - ) + _republish_to(item, item_process, item_status) def fake_process(controller): items = controller.get_items() for item in items.values(): - item_process = RepublishItemStatus(item) - _process_item(item, item_process) - if item_process.failed: + item_process = ProjectPushItemProcess(item) + item_status = ProjectPushItemStatus(item) + _process_item(item, item_process, item_status) + if item_status.failed: print("Process failed") else: print("Process Finished") @@ -414,15 +840,16 @@ def main(): # NOTE For development purposes controller = RepublisherController() project_name = "" - representation_id = "" + verssion_id = "" dst_project_name = "" dst_asset_id = "" dst_task_name = "" - controller.add_item(RepublishItem( + controller.add_item(ProjectPushItem( project_name, - representation_id, + version_id, dst_project_name, dst_asset_id, - dst_task_name + dst_task_name, + dst_version=1 )) fake_process(controller) \ No newline at end of file From 6f3059050f55d3110f6946f54af2d7ac096b872a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 5 Jan 2023 10:25:57 +0100 Subject: [PATCH 07/24] moved content to different subfolder --- .../republisher_dialog => push_to_project}/__init__.py | 0 .../control.py => push_to_project/control_context.py} | 0 .../control.py => push_to_project/control_integrate.py} | 0 .../republisher_dialog => push_to_project}/window.py | 2 +- openpype/tools/republisher/republisher_processor/__init__.py | 0 5 files changed, 1 insertion(+), 1 deletion(-) rename openpype/tools/{republisher/republisher_dialog => push_to_project}/__init__.py (100%) rename openpype/tools/{republisher/republisher_dialog/control.py => push_to_project/control_context.py} (100%) rename openpype/tools/{republisher/republisher_processor/control.py => push_to_project/control_integrate.py} (100%) rename openpype/tools/{republisher/republisher_dialog => push_to_project}/window.py (99%) delete mode 100644 openpype/tools/republisher/republisher_processor/__init__.py diff --git a/openpype/tools/republisher/republisher_dialog/__init__.py b/openpype/tools/push_to_project/__init__.py similarity index 100% rename from openpype/tools/republisher/republisher_dialog/__init__.py rename to openpype/tools/push_to_project/__init__.py diff --git a/openpype/tools/republisher/republisher_dialog/control.py b/openpype/tools/push_to_project/control_context.py similarity index 100% rename from openpype/tools/republisher/republisher_dialog/control.py rename to openpype/tools/push_to_project/control_context.py diff --git a/openpype/tools/republisher/republisher_processor/control.py b/openpype/tools/push_to_project/control_integrate.py similarity index 100% rename from openpype/tools/republisher/republisher_processor/control.py rename to openpype/tools/push_to_project/control_integrate.py diff --git a/openpype/tools/republisher/republisher_dialog/window.py b/openpype/tools/push_to_project/window.py similarity index 99% rename from openpype/tools/republisher/republisher_dialog/window.py rename to openpype/tools/push_to_project/window.py index cfc236ad27..99a77caa9e 100644 --- a/openpype/tools/republisher/republisher_dialog/window.py +++ b/openpype/tools/push_to_project/window.py @@ -11,7 +11,7 @@ from openpype.tools.utils import ( set_style_property, ) -from .control import PushToContextController +from .control_context import PushToContextController PROJECT_NAME_ROLE = QtCore.Qt.UserRole + 1 ASSET_NAME_ROLE = QtCore.Qt.UserRole + 2 diff --git a/openpype/tools/republisher/republisher_processor/__init__.py b/openpype/tools/republisher/republisher_processor/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 From 4762ff30e30cfe9b797178c15fa618709fe584c8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 5 Jan 2023 15:42:39 +0100 Subject: [PATCH 08/24] implemented rest of integration logic --- .../push_to_project/control_integrate.py | 1286 ++++++++++------- 1 file changed, 736 insertions(+), 550 deletions(-) diff --git a/openpype/tools/push_to_project/control_integrate.py b/openpype/tools/push_to_project/control_integrate.py index 36e78bfdc6..97de03ee1f 100644 --- a/openpype/tools/push_to_project/control_integrate.py +++ b/openpype/tools/push_to_project/control_integrate.py @@ -3,9 +3,13 @@ import re import copy import socket import itertools +import datetime + +from bson.objectid import ObjectId from openpype.client import ( get_project, + get_assets, get_asset_by_id, get_subset_by_id, get_subset_by_name, @@ -13,19 +17,25 @@ from openpype.client import ( get_last_version_by_subset_id, get_version_by_name, get_representations, - get_representation_by_name, ) from openpype.client.operations import ( OperationsSession, + new_asset_document, new_subset_document, new_version_doc, + new_representation_doc, prepare_version_update_data, + prepare_representation_update_data, ) +from openpype.modules import ModulesManager from openpype.lib import ( StringTemplate, get_openpype_username, get_formatted_current_time, + source_hash, ) + +from openpype.lib.file_transaction import FileTransaction from openpype.settings import get_project_settings from openpype.pipeline import Anatomy from openpype.pipeline.template_data import get_template_data @@ -35,10 +45,44 @@ from openpype.pipeline.create import get_subset_name UNKNOWN = object() -class RepublishError(Exception): +class PushToProjectError(Exception): pass +class FileItem(object): + def __init__(self, path): + self.path = path + + @property + def is_valid_file(self): + return os.path.exists(self.path) and os.path.isfile(self.path) + + +class SourceFile(FileItem): + def __init__(self, path, frame=None, udim=None): + super(SourceFile, self).__init__(path) + self.frame = frame + self.udim = udim + + def __repr__(self): + subparts = [self.__class__.__name__] + if self.frame is not None: + subparts.append("frame: {}".format(self.frame)) + if self.udim is not None: + subparts.append("UDIM: {}".format(self.udim)) + + return "<{}> '{}'".format(" - ".join(subparts), self.path) + + +class ResourceFile(FileItem): + def __init__(self, path, relative_path): + super(ResourceFile, self).__init__(path) + self.relative_path = relative_path + + def __repr__(self): + return "<{}> '{}'".format(self.__class__.__name__, self.relative_path) + + class ProjectPushItem: def __init__( self, @@ -47,6 +91,9 @@ class ProjectPushItem: dst_project_name, dst_asset_id, dst_task_name, + variant, + comment=None, + new_asset_name=None, dst_version=None ): self.src_project_name = src_project_name @@ -55,12 +102,17 @@ class ProjectPushItem: self.dst_asset_id = dst_asset_id self.dst_task_name = dst_task_name self.dst_version = dst_version + self.variant = variant + self.new_asset_name = new_asset_name + self.comment = comment or "" self._id = "|".join([ src_project_name, src_version_id, dst_project_name, - dst_asset_id, - dst_task_name + str(dst_asset_id), + str(new_asset_name), + str(dst_task_name), + str(dst_version) ]) @property @@ -68,13 +120,62 @@ class ProjectPushItem: return self._id def __repr__(self): - return "{} - {} -> {}/{}/{}".format( - self.src_project_name, - self.src_version_id, - self.dst_project_name, - self.dst_asset_id, - self.dst_task_name - ) + return "<{} - {}>".format(self.__class__.__name__, self.id) + + +class ProjectPushItemStatus: + def __init__( + self, failed=False, finished=False, error=None, messages=None + ): + if messages is None: + messages = [] + self._failed = failed + self._finished = finished + self._error = error + self._progress_messages = messages + self._last_message = None + + def get_failed(self): + return self._failed + + def set_failed(self, failed): + if failed == self._failed: + return + self._failed = failed + + def get_finished(self): + return self._finished + + def set_finished(self, finished): + if finished == self._finished: + return + self._finished = finished + + def get_error(self): + return self._error + + def set_error(self, error, failed=None): + if error == self._error: + return + self._error = error + if failed is None: + failed = error is not None + + if failed: + self.failed = failed + + failed = property(get_failed, set_failed) + finished = property(get_finished, set_finished) + error = property(get_error, set_error) + + def add_progress_message(self, message): + self._progress_messages.append(message) + self._last_message = message + print(message) + + @property + def last_message(self): + return self._last_message class ProjectPushRepreItem: @@ -90,6 +191,7 @@ class ProjectPushRepreItem: self._roots = roots self._src_files = None self._resource_files = None + self._frame = UNKNOWN @property def repre_doc(self): @@ -107,6 +209,20 @@ class ProjectPushRepreItem: self.get_source_files() return self._resource_files + @property + def frame(self): + if self._frame is UNKNOWN: + frame = None + for src_file in self.src_files: + src_frame = src_file.frame + if ( + src_frame is not None + and (frame is None or src_frame < frame) + ): + frame = src_frame + self._frame = frame + return self._frame + @staticmethod def validate_source_files(src_files, resource_files): if not src_files: @@ -233,623 +349,693 @@ class ProjectPushItemProcess: item (ProjectPushItem): Item which is being processed. """ - # TODO how to define 'variant'? - ask user - variant = "Main" # TODO where to get host?!!! host_name = "republisher" def __init__(self, item): self._item = item - self._src_project_doc = UNKNOWN - self._src_asset_doc = UNKNOWN - self._src_subset_doc = UNKNOWN - self._src_version_doc = UNKNOWN - self._src_repre_items = UNKNOWN + + self._src_project_doc = None + self._src_asset_doc = None + self._src_subset_doc = None + self._src_version_doc = None + self._src_repre_items = None self._src_anatomy = None - self._project_doc = UNKNOWN + self._project_doc = None self._anatomy = None - self._asset_doc = UNKNOWN - self._task_info = UNKNOWN + self._asset_doc = None + self._created_asset_doc = None + self._task_info = None self._subset_doc = None self._version_doc = None - self._family = UNKNOWN - self._subset_name = UNKNOWN + self._family = None + self._subset_name = None - self._project_settings = UNKNOWN - self._template_name = UNKNOWN + self._project_settings = None + self._template_name = None - self._src_files = UNKNOWN - self._src_resource_files = UNKNOWN + self._status = ProjectPushItemStatus() + self._operations = OperationsSession() + self._file_transaction = FileTransaction() - def get_src_project_doc(self): - if self._src_project_doc is UNKNOWN: - self._src_project_doc = get_project(self._item.src_project_name) + @property + def src_project_doc(self): return self._src_project_doc - def get_src_anatomy(self): - if self._src_anatomy is None: - self._src_anatomy = Anatomy(self._item.src_project_name) + @property + def src_anatomy(self): return self._src_anatomy - def get_src_asset_doc(self): - if self._src_asset_doc is UNKNOWN: - asset_doc = None - subset_doc = self.get_src_subset_doc() - if subset_doc: - asset_doc = get_asset_by_id( - self._item.src_project_name, - subset_doc["parent"] - ) - self._src_asset_doc = asset_doc + @property + def src_asset_doc(self): return self._src_asset_doc - def get_src_subset_doc(self): - if self._src_subset_doc is UNKNOWN: - version_doc = self.get_src_version_doc() - subset_doc = None - if version_doc: - subset_doc = get_subset_by_id( - self._item.src_project_name, - version_doc["parent"] - ) - self._src_subset_doc = subset_doc + @property + def src_subset_doc(self): return self._src_subset_doc - def get_src_version_doc(self): - if self._src_version_doc is UNKNOWN: - self._src_version_doc = get_version_by_id( - self._item.src_project_name, self._item.src_version_id - ) + @property + def src_version_doc(self): return self._src_version_doc - def get_src_repre_items(self): - if self._src_repre_items is UNKNOWN: - repre_items = None - version_doc = self.get_src_version_doc() - if version_doc: - repre_docs = get_representations( - self._item.src_project_name, - version_ids=[version_doc["_id"]] - ) - repre_items = [ - ProjectPushRepreItem(repre_doc, self.src_anatomy.roots) - for repre_doc in repre_docs - ] - self._src_repre_items = repre_items + @property + def src_repre_items(self): return self._src_repre_items - src_project_doc = property(get_src_project_doc) - src_anatomy = property(get_src_anatomy) - src_asset_doc = property(get_src_asset_doc) - src_subset_doc = property(get_src_subset_doc) - src_version_doc = property(get_src_version_doc) - src_repre_items = property(get_src_repre_items) - - def get_project_doc(self): - if self._project_doc is UNKNOWN: - self._project_doc = get_project(self._item.dst_project_name) + @property + def project_doc(self): return self._project_doc - def get_anatomy(self): - if self._anatomy is None: - self._anatomy = Anatomy(self._item.dst_project_name) + @property + def anatomy(self): return self._anatomy - def get_asset_doc(self): - if self._asset_doc is UNKNOWN: - self._asset_doc = get_asset_by_id( - self._item.dst_project_name, self._item.dst_asset_id - ) - return self._asset_doc - - def get_task_info(self): - if self._task_info is UNKNOWN: - task_name = self._item.dst_task_name - if not task_name: - self._task_info = {} - return self._task_info - - project_doc = self.get_project_doc() - asset_doc = self.get_asset_doc() - if not project_doc or not asset_doc: - self._task_info = None - return self._task_info - - asset_tasks = asset_doc.get("data", {}).get("tasks") or {} - task_info = asset_tasks.get(task_name) - if not task_info: - self._task_info = None - return self._task_info - - # Create copy of task info to avoid changing data in asset document - task_info = copy.deepcopy(task_info) - task_info["name"] = task_name - # Fill rest of task information based on task type - task_type = task_info["type"] - task_type_info = project_doc["config"]["tasks"].get(task_type, {}) - task_info.update(task_type_info) - self._task_info = task_info - - return self._task_info - - def get_subset_doc(self): - return self._subset_doc - - def set_subset_doc(self, subset_doc): - self._subset_doc = subset_doc - - def get_version_doc(self): - return self._version_doc - - def set_version_doc(self, version_doc): - self._version_doc = version_doc - - project_doc = property(get_project_doc) - anatomy = property(get_anatomy) - asset_doc = property(get_asset_doc) - task_info = property(get_task_info) - subset_doc = property(get_subset_doc) - version_doc = property(get_version_doc, set_version_doc) - - def get_project_settings(self): - if self._project_settings is UNKNOWN: - self._project_settings = get_project_settings( - self._item.dst_project_name - ) + @property + def project_settings(self): return self._project_settings - project_settings = property(get_project_settings) + @property + def asset_doc(self): + return self._asset_doc + + @property + def task_info(self): + return self._task_info + + @property + def subset_doc(self): + return self._subset_doc + + @property + def version_doc(self): + return self._version_doc + + @property + def variant(self): + return self._item.variant @property def family(self): - if self._family is UNKNOWN: - family = None - subset_doc = self.src_subset_doc - if subset_doc: - family = subset_doc["data"].get("family") - families = subset_doc["data"].get("families") - if not family and families: - family = families[0] - self._family = family return self._family @property def subset_name(self): - if self._subset_name is UNKNOWN: - subset_name = None - family = self.family - asset_doc = self.asset_doc - task_info = self.task_info - if family and asset_doc and task_info: - subset_name = get_subset_name( - family, - self.variant, - task_info["name"], - asset_doc, - project_name=self._item.dst_project_name, - host_name=self.host_name, - project_settings=self.project_settings - ) - self._subset_name = subset_name return self._subset_name @property def template_name(self): - if self._template_name is UNKNOWN: - task_info = self.task_info - family = self.family - template_name = None - if family and task_info: - template_name = get_publish_template_name( - self._item.dst_project_name, - self.host_name, - self.family, - task_info["name"], - task_info["type"], - project_settings=self.project_settings - ) - self._template_name = template_name return self._template_name + def fill_source_variables(self): + src_project_name = self._item.src_project_name + src_version_id = self._item.src_version_id -class ProjectPushItemStatus: - def __init__( - self, - item, - failed=False, - finished=False, - error=None - ): - self._item = item - self._failed = failed - self._finished = finished - self._error = error - self._progress_messages = [] - self._last_message = None - - def get_failed(self): - return self._failed - - def set_failed(self, failed): - if failed == self._failed: - return - self._failed = failed - - def get_finished(self): - return self._finished - - def set_finished(self, finished): - if finished == self._finished: - return - self._finished = finished - - def get_error(self): - return self._error - - def set_error(self, error, failed=None): - if error == self._error: - return - self._error = error - if failed is None: - failed = error is not None - - if failed: - self.failed = failed - - failed = property(get_failed, set_failed) - finished = property(get_finished, set_finished) - error = property(get_error, set_error) - - def add_progress_message(self, message): - self._progress_messages.append(message) - self._last_message = message - print(message) - - @property - def last_message(self): - return self._last_message - - -class RepublisherController: - def __init__(self): - self._items = {} - - def add_item(self, item): - if item.id in self._items: - raise RepublishError(f"Item is already in queue {item}") - self._items[item.id] = item - - def remote_item(self, item_id): - self._items.pop(item_id, None) - - def get_items(self): - return dict(self._items) - - -class FileItem(object): - def __init__(self, path): - self.path = path - - @property - def is_valid_file(self): - return os.path.exists(self.path) and os.path.isfile(self.path) - - -class SourceFile(FileItem): - def __init__(self, path, frame=None, udim=None): - super(SourceFile, self).__init__(path) - self.frame = frame - self.udim = udim - - def __repr__(self): - subparts = [self.__class__.__name__] - if self.frame is not None: - subparts.append("frame: {}".format(self.frame)) - if self.udim is not None: - subparts.append("UDIM: {}".format(self.udim)) - - return "<{}> '{}'".format(" - ".join(subparts), self.path) - - -class ResourceFile(FileItem): - def __init__(self, path, relative_path): - super(ResourceFile, self).__init__(path) - self.relative_path = relative_path - - def __repr__(self): - return "<{}> '{}'".format(self.__class__.__name__, self.relative_path) - - -def _make_sure_subset_exists(item_process, project_name, operations): - dst_asset_doc = item_process.asset_doc - subset_name = item_process.subset_name - family = item_process.family - asset_id = dst_asset_doc["_id"] - subset_doc = get_subset_by_name(project_name, subset_name, asset_id) - if subset_doc: - return subset_doc - - data = { - "families": [family] - } - subset_doc = new_subset_document( - subset_name, family, asset_id, data - ) - operations.create_entity(project_name, "subset", subset_doc) - item_process.set_subset_doc(subset_doc) - - -def _make_sure_version_exists( - item_process, - project_name, - version, - operations -): - """Make sure version document exits in database. - - Args: - item_process (ProjectPushItemProcess): Item handling process. - project_name (str): Name of project where version should live. - version (Union[int, None]): Number of version. Latest is used when - 'None' is passed. - operations (OperationsSession): Session which handler creation and - update of entities. - - Returns: - Tuple[Dict[str, Any], bool]: New version document and boolean if version - already existed in database. - """ - - src_version_doc = item_process.src_version_doc - subset_doc = item_process.subset_doc - subset_id = subset_doc["_id"] - src_data = src_version_doc["data"] - families = subset_doc["data"].get("families") - if not families: - families = [subset_doc["data"]["family"]] - - version_data = { - "families": list(families), - "fps": src_data.get("fps"), - "source": src_data.get("source"), - "machine": socket.gethostname(), - "comment": "", - "author": get_openpype_username(), - "time": get_formatted_current_time(), - } - if version is None: - last_version_doc = get_last_version_by_subset_id( - project_name, subset_id - ) - version = 1 - if last_version_doc: - version += int(last_version_doc["name"]) - - existing_version_doc = get_version_by_name( - project_name, version, subset_id - ) - # Update existing version - if existing_version_doc: - version_doc = new_version_doc( - version, subset_id, version_data, existing_version_doc["_id"] - ) - update_data = prepare_version_update_data( - existing_version_doc, version_doc - ) - if update_data: - operations.update_entity( - project_name, - "version", - existing_version_doc["_id"], - update_data + project_doc = get_project(src_project_name) + if not project_doc: + self._status.error = ( + f"Source project \"{src_project_name}\" was not found" ) - item_process.set_version_doc(version_doc) + raise PushToProjectError(self._status.error) - return - - if version is None: - last_version_doc = get_last_version_by_subset_id( - project_name, subset_id + self._status.add_progress_message( + f"Project '{src_project_name}' found" ) - version = 1 - if last_version_doc: - version += int(last_version_doc["name"]) - version_doc = new_version_doc( - version, subset_id, version_data - ) - operations.create_entity(project_name, "version", version_doc) + version_doc = get_version_by_id(src_project_name, src_version_id) + if not version_doc: + self._status.error = ( + f"Source version with id \"{src_version_id}\"" + f" was not found in project \"{src_project_name}\"" + ) + raise PushToProjectError(self._status.error) - item_process.set_version_doc(version_doc) + subset_id = version_doc["parent"] + subset_doc = get_subset_by_id(src_project_name, subset_id) + if not subset_doc: + self._status.error = ( + f"Could find subset with id \"{subset_id}\"" + f" in project \"{src_project_name}\"" + ) + raise PushToProjectError(self._status.error) + asset_id = subset_doc["parent"] + asset_doc = get_asset_by_id(src_project_name, asset_id) + if not asset_doc: + self._status.error = ( + f"Could find asset with id \"{asset_id}\"" + f" in project \"{src_project_name}\"" + ) + raise PushToProjectError(self._status.error) -def _integrate_representations(item, item_process, item_status, operations): - """ + anatomy = Anatomy(src_project_name) - Args: - item (ProjectPushItem): Item to be pushed to different project. - item_process (ProjectPushItemProcess): Process of push item. - """ - - version_id = item_process.version_doc["_id"] - repre_names = { - repre_item.repre_doc["name"] - for repre_item in item_process.src_repre_items - } - existing_repres = get_representations( - item.dst_project_name, - representation_names=repre_names, - version_ids=[version_id] - ) - existing_repres_by_name = { - repre_doc["name"] : repre_doc - for repre_doc in existing_repres - } - anatomy = item_process.anatomy - formatting_data = get_template_data( - item_process.project_doc, - item_process.asset_doc, - item.dst_task_name, - item_process.host_name - ) - - -def _republish_to(item, item_process, item_status): - """ - - Args: - item (ProjectPushItem): Item to process. - item_process (ProjectPushItemProcess): Item process information. - item_status (ProjectPushItemStatus): Item status information. - """ - - family = item_process.family - item_status.add_progress_message( - f"Republishing family '{family}' (Based on source subset)" - ) - - subset_name = item_process.subset_name - item_status.add_progress_message(f"Final subset name is '{subset_name}'") - - template_name = item_process.template_name - item_status.add_progress_message( - f"Using template '{template_name}' for integration" - ) - - repre_items = item_process.src_repre_items - file_count = sum( - len(repre_item.src_files) + len(repre_item.resource_files) - for repre_item in repre_items - ) - item_status.add_progress_message( - f"Representation has {file_count} files to integrate" - ) - - operations = OperationsSession() - item_status.add_progress_message( - f"Integration to {item.dst_project_name} begins." - ) - _make_sure_subset_exists( - item_process, - item.dst_project_name, - operations - ) - _make_sure_version_exists( - item_process, - item.dst_project_name, - item.dst_version, - operations - ) - _integrate_representations(item, item_process, item_status, operations) - - -def _process_item(item, item_process, item_status): - """ - - Args: - item (ProjectPushItem): Item defying the source and destination. - item_process (ProjectPushItemProcess): Process item. - item_status (ProjectPushItemStatus): Status of process item. - """ - - # Query all entities source and destination - # - all of them are required for processing to exist - # --- Source entities --- - # Project - we just need validation of existence - src_project_name = item.src_project_name - src_project_doc = item_process.get_src_project_doc() - if not src_project_doc: - item_status.error = ( - f"Source project '{src_project_name}' was not found" + repre_docs = get_representations( + src_project_name, + version_ids=[src_version_id] ) - return - item_status.add_progress_message(f"Project '{src_project_name}' found") + repre_items = [ + ProjectPushRepreItem(repre_doc, anatomy.roots) + for repre_doc in repre_docs + ] + self._status.add_progress_message(( + f"Found {len(repre_items)} representations on" + f" version {src_version_id} in project '{src_project_name}'" + )) - # Representation - contains information of source files and template data - repre_items = item_process.get_src_repre_items() - if not repre_items: - item_status.error = ( - f"Version {item.src_version_id} does not have any representations" + self._src_anatomy = anatomy + self._src_project_doc = project_doc + self._src_asset_doc = asset_doc + self._src_subset_doc = subset_doc + self._src_version_doc = version_doc + self._src_repre_items = repre_items + + def fill_destination_project(self): + # --- Destination entities --- + dst_project_name = self._item.dst_project_name + # Validate project existence + dst_project_doc = get_project(dst_project_name) + if not dst_project_doc: + self._status.error = ( + f"Destination project '{dst_project_name}' was not found" + ) + raise PushToProjectError(self._status.error) + + self._status.add_progress_message( + f"Destination project '{dst_project_name}' found" ) - return - - item_status.add_progress_message( - f"Found {len(repre_items)} representations on" - f" version {item.src_version_id} in project '{src_project_name}'" - ) - - # --- Destination entities --- - dst_project_name = item.dst_project_name - dst_asset_id = item.dst_asset_id - dst_task_name = item.dst_task_name - - # Validate project existence - dst_project_doc = item_process.get_project_doc() - if not dst_project_doc: - item_status.error = ( - f"Destination project '{dst_project_name}' was not found" + self._project_doc = dst_project_doc + self._anatomy = Anatomy(dst_project_name) + self._project_settings = get_project_settings( + self._item.dst_project_name ) - return - item_status.add_progress_message(f"Project '{dst_project_name}' found") - # Get asset document - if not item_process.asset_doc: - item_status.error = ( - f"Destination asset with id '{dst_asset_id}'" - f" was not found in project '{dst_project_name}'" + def _create_asset( + self, + src_asset_doc, + project_doc, + parent_asset_doc, + asset_name + ): + parent_id = None + parents = [] + tools = [] + if parent_asset_doc: + parent_id = parent_asset_doc["_id"] + parents = list(parent_asset_doc["data"]["parents"]) + parents.append(parent_asset_doc["name"]) + _tools = parent_asset_doc["data"].get("tools_env") + if _tools: + tools = list(_tools) + + asset_name_low = asset_name.lower() + other_asset_docs = get_assets( + project_doc["name"], parent_ids=[parent_id], fields=["name"] ) - return - item_status.add_progress_message(( - f"Asset with id '{dst_asset_id}'" - f" found in project '{dst_project_name}'" - )) + for other_asset_doc in other_asset_docs: + other_name = other_asset_doc["name"] + if other_name.lower() == asset_name_low: + self._status.add_progress_message( + f"Found already existing asset with name \"{other_name}\"" + f" which match requested name \"{asset_name}\"" + ) + return other_asset_doc - # Get task information from asset document - if not item_process.task_info: - item_status.error = ( - f"Destination task '{dst_task_name}'" - f" was not found on asset with id '{dst_asset_id}'" - f" in project '{dst_project_name}'" + data_keys = ( + "clipIn", + "clipOut", + "frameStart", + "frameEnd", + "handleStart", + "handleEnd", + "resolutionWidth", + "resolutionHeight", + "fps", + "pixelAspect", ) - return + asset_data = { + "visualParent": parent_id, + "parents": parents, + "tasks": {}, + "tools_env": tools + } + src_asset_data = src_asset_doc["data"] + for key in data_keys: + if key in src_asset_data: + asset_data[key] = src_asset_data[key] - item_status.add_progress_message(( - f"Task with name '{dst_task_name}'" - f" found on asset with id '{dst_asset_id}'" - f" in project '{dst_project_name}'" - )) + asset_doc = new_asset_document( + asset_name, + project_doc["_id"], + parent_id, + parents, + data=asset_data + ) + self._operations.create_entity( + project_doc["name"], + asset_doc["type"], + asset_doc + ) + self._status.add_progress_message( + f"Creating new asset with name \"{asset_name}\"" + ) + self._created_asset_doc = asset_doc + return asset_doc - _republish_to(item, item_process, item_status) + def fill_or_create_destination_asset(self): + dst_project_name = self._item.dst_project_name + dst_asset_id = self._item.dst_asset_id + dst_task_name = self._item.dst_task_name + new_asset_name = self._item.new_asset_name + if not dst_asset_id and not new_asset_name: + self._status.error = ( + "Push item does not have defined destination asset" + ) + raise PushToProjectError(self._status.error) + # Get asset document + parent_asset_doc = None + if dst_asset_id: + parent_asset_doc = get_asset_by_id( + self._item.dst_project_name, self._item.dst_asset_id + ) + if not parent_asset_doc: + self._status.error = ( + f"Could find asset with id \"{dst_asset_id}\"" + f" in project \"{dst_project_name}\"" + ) + raise PushToProjectError(self._status.error) -def fake_process(controller): - items = controller.get_items() - for item in items.values(): - item_process = ProjectPushItemProcess(item) - item_status = ProjectPushItemStatus(item) - _process_item(item, item_process, item_status) - if item_status.failed: - print("Process failed") + if not new_asset_name: + asset_doc = parent_asset_doc else: - print("Process Finished") + asset_doc = self._create_asset( + self.src_asset_doc, + self.project_doc, + parent_asset_doc, + new_asset_name + ) + self._asset_doc = asset_doc + if not dst_task_name: + self._task_info = {} + return + + asset_path_parts = list(asset_doc["data"]["parents"]) + asset_path_parts.append(asset_doc["name"]) + asset_path = "/".join(asset_path_parts) + asset_tasks = asset_doc.get("data", {}).get("tasks") or {} + task_info = asset_tasks.get(dst_task_name) + if not task_info: + self._status.error = ( + f"Could find task with name \"{dst_task_name}\"" + f" on asset \"{asset_path}\"" + f" in project \"{dst_project_name}\"" + ) + raise PushToProjectError(self._status.error) + + # Create copy of task info to avoid changing data in asset document + task_info = copy.deepcopy(task_info) + task_info["name"] = dst_task_name + # Fill rest of task information based on task type + task_type = task_info["type"] + task_type_info = self.project_doc["config"]["tasks"].get(task_type, {}) + task_info.update(task_type_info) + self._task_info = task_info + + def determine_family(self): + subset_doc = self.src_subset_doc + family = subset_doc["data"].get("family") + families = subset_doc["data"].get("families") + if not family and families: + family = families[0] + + if not family: + self._status.error = ( + "Couldn't figure out family from source subset" + ) + raise PushToProjectError(self._status.error) + + self._status.add_progress_message( + f"Publishing family is '{family}' (Based on source subset)" + ) + self._family = family + + def determine_publish_template_name(self): + template_name = get_publish_template_name( + self._item.dst_project_name, + self.host_name, + self.family, + self.task_info.get("name"), + self.task_info.get("type"), + project_settings=self.project_settings + ) + self._status.add_progress_message( + f"Using template '{template_name}' for integration" + ) + self._template_name = template_name + + def determine_subset_name(self): + family = self.family + asset_doc = self.asset_doc + task_info = self.task_info + subset_name = get_subset_name( + family, + self.variant, + task_info.get("name"), + asset_doc, + project_name=self._item.dst_project_name, + host_name=self.host_name, + project_settings=self.project_settings + ) + self._status.add_progress_message( + f"Push will be integrating to subet with name '{subset_name}'" + ) + self._subset_name = subset_name + + def make_sure_subset_exists(self): + project_name = self._item.dst_project_name + asset_id = self.asset_doc["_id"] + subset_name = self.subset_name + family = self.family + subset_doc = get_subset_by_name(project_name, subset_name, asset_id) + if subset_doc: + self._subset_doc = subset_doc + return subset_doc + + data = { + "families": [family] + } + subset_doc = new_subset_document( + subset_name, family, asset_id, data + ) + self._operations.create_entity(project_name, "subset", subset_doc) + self._subset_doc = subset_doc + + def make_sure_version_exists(self): + """Make sure version document exits in database. + + Args: + item_process (ProjectPushItemProcess): Item handling process. + project_name (str): Name of project where version should live. + version (Union[int, None]): Number of version. Latest is used when + 'None' is passed. + operations (OperationsSession): Session which handler creation and + update of entities. + + Returns: + Tuple[Dict[str, Any], bool]: New version document and boolean if version + already existed in database. + """ + + project_name = self._item.dst_project_name + version = self._item.dst_version + src_version_doc = self.src_version_doc + subset_doc = self.subset_doc + subset_id = subset_doc["_id"] + src_data = src_version_doc["data"] + families = subset_doc["data"].get("families") + if not families: + families = [subset_doc["data"]["family"]] + + version_data = { + "families": list(families), + "fps": src_data.get("fps"), + "source": src_data.get("source"), + "machine": socket.gethostname(), + "comment": "", + "author": get_openpype_username(), + "time": get_formatted_current_time(), + } + if version is None: + last_version_doc = get_last_version_by_subset_id( + project_name, subset_id + ) + version = 1 + if last_version_doc: + version += int(last_version_doc["name"]) + + existing_version_doc = get_version_by_name( + project_name, version, subset_id + ) + # Update existing version + if existing_version_doc: + version_doc = new_version_doc( + version, subset_id, version_data, existing_version_doc["_id"] + ) + update_data = prepare_version_update_data( + existing_version_doc, version_doc + ) + if update_data: + self._operations.update_entity( + project_name, + "version", + existing_version_doc["_id"], + update_data + ) + self._version_doc = version_doc + + return + + if version is None: + last_version_doc = get_last_version_by_subset_id( + project_name, subset_id + ) + version = 1 + if last_version_doc: + version += int(last_version_doc["name"]) + + version_doc = new_version_doc( + version, subset_id, version_data + ) + self._operations.create_entity(project_name, "version", version_doc) + + self._version_doc = version_doc + + def integrate_representations(self): + try: + self._integrate_representations() + except Exception: + self._operations.clear() + self._file_transaction.rollback() + raise + + def _integrate_representations(self): + version_doc = self.version_doc + version_id = version_doc["_id"] + existing_repres = get_representations( + self._item.dst_project_name, + version_ids=[version_id] + ) + existing_repres_by_low_name = { + repre_doc["name"].lower(): repre_doc + for repre_doc in existing_repres + } + template_name = self.template_name + anatomy = self.anatomy + formatting_data = get_template_data( + self.project_doc, + self.asset_doc, + self.task_info.get("name"), + self.host_name + ) + formatting_data.update({ + "subset": self.subset_name, + "family": self.family, + "version": version_doc["name"] + }) + + path_template = anatomy.templates[template_name]["path"].replace( + "\\", "/" + ) + file_template = StringTemplate( + anatomy.templates[template_name]["file"] + ) + processed_repre_items = self._prepare_file_transactions( + anatomy, template_name, formatting_data, file_template + ) + self._file_transaction.process() + self._prepare_database_operations( + version_id, + processed_repre_items, + path_template, + existing_repres_by_low_name + ) + self._operations.commit() + self._file_transaction.finalize() + + def _prepare_file_transactions( + self, anatomy, template_name, formatting_data, file_template + ): + processed_repre_items = [] + for repre_item in self.src_repre_items: + repre_doc = repre_item.repre_doc + repre_name = repre_doc["name"] + repre_format_data = copy.deepcopy(formatting_data) + repre_format_data["representation"] = repre_name + for src_file in repre_item.src_files: + ext = os.path.splitext(src_file.path)[-1] + repre_format_data["ext"] = ext[1:] + break + + tmp_result = anatomy.format(formatting_data) + folder_path = tmp_result[template_name]["folder"] + repre_context = folder_path.used_values + folder_path_rootless = folder_path.rootless + repre_filepaths = [] + published_path = None + for src_file in repre_item.src_files: + file_data = copy.deepcopy(repre_format_data) + frame = src_file.frame + if frame is not None: + file_data["frame"] = frame + + udim = src_file.udim + if udim is not None: + file_data["udim"] = udim + + filename = file_template.format_strict(file_data) + dst_filepath = os.path.normpath( + os.path.join(folder_path, filename) + ) + dst_rootless_path = os.path.normpath( + os.path.join(folder_path_rootless, filename) + ) + 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) + + for resource_file in repre_item.resource_files: + dst_filepath = os.path.normpath( + os.path.join(folder_path, resource_file.relative_path) + ) + dst_rootless_path = os.path.normpath( + os.path.join( + folder_path_rootless, resource_file.relative_path + ) + ) + repre_filepaths.append((dst_filepath, dst_rootless_path)) + self._file_transaction.add(resource_file.path, dst_filepath) + processed_repre_items.append( + (repre_item, repre_filepaths, repre_context, published_path) + ) + return processed_repre_items + + def _prepare_database_operations( + self, + version_id, + processed_repre_items, + path_template, + existing_repres_by_low_name + ): + modules_manager = ModulesManager() + sync_server_module = modules_manager.get("sync_server") + if sync_server_module is None or not sync_server_module.enabled: + sites = [{ + "name": "studio", + "created_dt": datetime.datetime.now() + }] + else: + sites = sync_server_module.compute_resource_sync_sites( + project_name=self._item.dst_project_name + ) + + added_repre_names = set() + for item in processed_repre_items: + (repre_item, repre_filepaths, repre_context, published_path) = item + repre_name = repre_item.repre_doc["name"] + added_repre_names.add(repre_name.lower()) + new_repre_data = { + "path": published_path, + "template": path_template + } + new_repre_files = [] + for (path, rootless_path) in repre_filepaths: + new_repre_files.append({ + "_id": ObjectId(), + "path": rootless_path, + "size": os.path.getsize(path), + "hash": source_hash(path), + "sites": sites + }) + + existing_repre = existing_repres_by_low_name.get( + repre_name.lower() + ) + entity_id = None + if existing_repre: + entity_id = existing_repre["_id"] + new_repre_doc = new_representation_doc( + repre_name, + version_id, + repre_context, + data=new_repre_data, + entity_id=entity_id + ) + new_repre_doc["files"] = new_repre_files + if not existing_repre: + self._operations.create_entity( + self._item.dst_project_name, + new_repre_doc["type"], + new_repre_doc + ) + else: + update_data = prepare_representation_update_data( + existing_repre, new_repre_doc + ) + if update_data: + self._operations.update_entity( + self._item.dst_project_name, + new_repre_doc["type"], + new_repre_doc["_id"], + update_data + ) + + existing_repre_names = set(existing_repres_by_low_name.keys()) + for repre_name in (existing_repre_names - added_repre_names): + repre_doc = existing_repres_by_low_name[repre_name] + self._operations.update_entity( + self._item.dst_project_name, + repre_doc["type"], + repre_doc["_id"], + {"type": "archived_representation"} + ) + + def process(self): + item_process.fill_source_variables() + item_process.fill_destination_project() + item_process.fill_or_create_destination_asset() + item_process.determine_family() + item_process.determine_publish_template_name() + item_process.determine_subset_name() + item_process.make_sure_subset_exists() + item_process.make_sure_version_exists() + item_process.integrate_representations() def main(): # NOTE For development purposes - controller = RepublisherController() project_name = "" - verssion_id = "" + version_id = "" dst_project_name = "" dst_asset_id = "" dst_task_name = "" - controller.add_item(ProjectPushItem( + version = None + variant = "" + comment = "" + + item = ProjectPushItem( project_name, version_id, dst_project_name, dst_asset_id, dst_task_name, + variant, + version, dst_version=1 - )) - fake_process(controller) \ No newline at end of file + ) + item_process = ProjectPushItemProcess(item) + item_process.process() From 854bf3a585e1a7baa7525d9c94daca67ddada851 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 5 Jan 2023 16:19:26 +0100 Subject: [PATCH 09/24] added more detailed helper functions to receive asset icon name and color --- openpype/tools/utils/__init__.py | 6 +++++ openpype/tools/utils/lib.py | 42 ++++++++++++++++++++++++++++---- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/openpype/tools/utils/__init__.py b/openpype/tools/utils/__init__.py index 31c8232f47..d51ebb5744 100644 --- a/openpype/tools/utils/__init__.py +++ b/openpype/tools/utils/__init__.py @@ -20,6 +20,9 @@ from .lib import ( DynamicQThread, qt_app_context, get_asset_icon, + get_asset_icon_by_name, + get_asset_icon_name_from_doc, + get_asset_icon_color_from_doc, ) from .models import ( @@ -53,6 +56,9 @@ __all__ = ( "DynamicQThread", "qt_app_context", "get_asset_icon", + "get_asset_icon_by_name", + "get_asset_icon_name_from_doc", + "get_asset_icon_color_from_doc", "RecursiveSortFilterProxyModel", diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index 5302946c28..04ab2d028f 100644 --- a/openpype/tools/utils/lib.py +++ b/openpype/tools/utils/lib.py @@ -168,20 +168,52 @@ def get_project_icon(project_doc): def get_asset_icon_name(asset_doc, has_children=True): - icon_name = asset_doc["data"].get("icon") + icon_name = get_asset_icon_name_from_doc(asset_doc) if icon_name: return icon_name + return get_default_asset_icon_name(has_children) + +def get_asset_icon_color(asset_doc): + icon_color = get_asset_icon_color_from_doc(asset_doc) + if icon_color: + return icon_color + return get_default_entity_icon_color() + + +def get_default_asset_icon_name(has_children): if has_children: return "fa.folder" return "fa.folder-o" -def get_asset_icon_color(asset_doc): - icon_color = asset_doc["data"].get("color") +def get_asset_icon_name_from_doc(asset_doc): + if asset_doc: + return asset_doc["data"].get("icon") + return None + + +def get_asset_icon_color_from_doc(asset_doc): + if asset_doc: + return asset_doc["data"].get("color") + return None + + +def get_asset_icon_by_name(icon_name, icon_color, has_children=False): + if not icon_name: + icon_name = get_default_asset_icon_name(has_children) + if icon_color: - return icon_color - return get_default_entity_icon_color() + icon_color = QtGui.QColor(icon_color) + else: + icon_color = get_default_entity_icon_color() + icon = get_qta_icon_by_name_and_color(icon_name, icon_color) + if icon is not None: + return icon + return get_qta_icon_by_name_and_color( + get_default_asset_icon_name(has_children), + icon_color + ) def get_asset_icon(asset_doc, has_children=False): From 3650ac489aabc4277ac2cef73893515888a28428 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 5 Jan 2023 17:07:20 +0100 Subject: [PATCH 10/24] small cleanup --- .../push_to_project/control_integrate.py | 309 ++++++++++++------ 1 file changed, 215 insertions(+), 94 deletions(-) diff --git a/openpype/tools/push_to_project/control_integrate.py b/openpype/tools/push_to_project/control_integrate.py index 97de03ee1f..e4ae9decd3 100644 --- a/openpype/tools/push_to_project/control_integrate.py +++ b/openpype/tools/push_to_project/control_integrate.py @@ -123,64 +123,150 @@ class ProjectPushItem: return "<{} - {}>".format(self.__class__.__name__, self.id) +class StatusMessage: + def __init__(self, message, level): + self.message = message + self.level = level + + def __str__(self): + return "{}: {}".format(self.level.upper(), self.message) + + def __repr__(self): + return "<{} - {}> {}".format( + self.__class__.__name__, self.level.upper, self.message + ) + + class ProjectPushItemStatus: def __init__( - self, failed=False, finished=False, error=None, messages=None + self, + failed=False, + finished=False, + fail_reason=None, + messages=None ): if messages is None: messages = [] self._failed = failed self._finished = finished - self._error = error - self._progress_messages = messages - self._last_message = None - - def get_failed(self): - return self._failed - - def set_failed(self, failed): - if failed == self._failed: - return - self._failed = failed + self._fail_reason = fail_reason + self._messages = messages def get_finished(self): + """Processing of push to project finished. + + Returns: + bool: Finished. + """ + return self._finished - def set_finished(self, finished): - if finished == self._finished: + def set_finished(self, finished=True): + """Mark status as finished. + + Args: + finished (bool): Processing finished (failed or not). + """ + + if finished != self._finished: + self._finished = finished + + def get_failed(self): + """Processing failed. + + Returns: + bool: Processing failed. + """ + + return self._failed + + def set_failed(self, failed, fail_reason=UNKNOWN): + """Set status as failed. + + Attribute 'fail_reason' can change automatically based on passed value. + Reason is unset if 'failed' is 'False' and is set do default reason if + is set to 'True' and reason is not set. + + Args: + failed (bool): Push to project failed. + fail_reason (str): Reason why failed. + """ + + self._failed = failed + if fail_reason is not UNKNOWN and self._fail_reason != fail_reason: + self.set_fail_reason(fail_reason) + + if failed and self._fail_reason is None: + self.set_fail_reason("Failed without specified reason") + + elif not failed and self._fail_reason: + self.set_fail_reason(None) + + def get_fail_reason(self): + """Reason why push to process failed. + + Returns: + Union[str, None]: Reason why push failed or None. + """ + + return self._fail_reason + + def set_fail_reason(self, reason): + """Mark process status as failed. + + Status is also set to failed if 'reason' is not None. + + Args: + reason (str): Reason why push to project failed. + """ + + if self._fail_reason == reason: return - self._finished = finished + self._fail_reason = reason + if reason and not self._failed: + self.set_failed(True) - def get_error(self): - return self._error + if reason: + print(f"Integration failed: {reason}") - def set_error(self, error, failed=None): - if error == self._error: - return - self._error = error - if failed is None: - failed = error is not None - - if failed: - self.failed = failed - - failed = property(get_failed, set_failed) finished = property(get_finished, set_finished) - error = property(get_error, set_error) + failed = property(get_failed, set_failed) + fail_reason = property(get_fail_reason, set_fail_reason) - def add_progress_message(self, message): - self._progress_messages.append(message) - self._last_message = message - print(message) + # Loggin helpers + # TODO better logging + def add_message(self, message, level): + message_obj = StatusMessage(message, level) + self._messages.append(message_obj) + print(message_obj) + return message_obj - @property - def last_message(self): - return self._last_message + def debug(self, message): + return self.add_message(message, "debug") + + def info(self, message): + return self.add_message(message, "info") + + def warning(self, message): + return self.add_message(message, "warning") + + def error(self, message): + return self.add_message(message, "error") + + def critical(self, message): + return self.add_message(message, "critical") class ProjectPushRepreItem: """Representation item. + Representation item based on representation document and project roots. + + Representation document may have reference to: + - source files: Files defined with publish template + - resource files: Files that should be in publish directory + but filenames are not template based. + Args: repre_doc (Dict[str, Ant]): Representation document. roots (Dict[str, str]): Project roots (based on project anatomy). @@ -211,6 +297,15 @@ class ProjectPushRepreItem: @property def frame(self): + """First frame of representation files. + + This value will be in representation document context if is sequence. + + Returns: + Union[int, None]: First frame in representation files based on + source files or None if frame is not part of filename. + """ + if self._frame is UNKNOWN: frame = None for src_file in self.src_files: @@ -263,6 +358,13 @@ class ProjectPushRepreItem: src_files = [] resource_files = [] template = self._repre_doc["data"]["template"] + # Remove padding from 'udim' and 'frame' formatting keys + # - "{frame:0>4}" -> "{frame}" + for key in ("udim", "frame"): + sub_part = "{" + key + "[^}]*}" + replacement = "{{{}}}".format(key) + template = re.sub(sub_part, replacement, template) + repre_context = self._repre_doc["context"] fill_repre_context = copy.deepcopy(repre_context) if "frame" in fill_repre_context: @@ -347,12 +449,13 @@ class ProjectPushItemProcess: """ Args: item (ProjectPushItem): Item which is being processed. + item_status (ProjectPushItemStatus): Object to store status. """ # TODO where to get host?!!! host_name = "republisher" - def __init__(self, item): + def __init__(self, item, item_status=None): self._item = item self._src_project_doc = None @@ -376,10 +479,16 @@ class ProjectPushItemProcess: self._project_settings = None self._template_name = None - self._status = ProjectPushItemStatus() + if item_status is None: + item_status = ProjectPushItemStatus() + self._status = item_status self._operations = OperationsSession() self._file_transaction = FileTransaction() + @property + def status(self): + return self._status + @property def src_project_doc(self): return self._src_project_doc @@ -454,40 +563,38 @@ class ProjectPushItemProcess: project_doc = get_project(src_project_name) if not project_doc: - self._status.error = ( + self._status.set_fail_reason( f"Source project \"{src_project_name}\" was not found" ) - raise PushToProjectError(self._status.error) + raise PushToProjectError(self._status.fail_reason) - self._status.add_progress_message( - f"Project '{src_project_name}' found" - ) + self._status.debug(f"Project '{src_project_name}' found") version_doc = get_version_by_id(src_project_name, src_version_id) if not version_doc: - self._status.error = ( + self._status.set_fail_reason(( f"Source version with id \"{src_version_id}\"" f" was not found in project \"{src_project_name}\"" - ) - raise PushToProjectError(self._status.error) + )) + raise PushToProjectError(self._status.fail_reason) subset_id = version_doc["parent"] subset_doc = get_subset_by_id(src_project_name, subset_id) if not subset_doc: - self._status.error = ( + self._status.set_fail_reason(( f"Could find subset with id \"{subset_id}\"" f" in project \"{src_project_name}\"" - ) - raise PushToProjectError(self._status.error) + )) + raise PushToProjectError(self._status.fail_reason) asset_id = subset_doc["parent"] asset_doc = get_asset_by_id(src_project_name, asset_id) if not asset_doc: - self._status.error = ( + self._status.set_fail_reason(( f"Could find asset with id \"{asset_id}\"" f" in project \"{src_project_name}\"" - ) - raise PushToProjectError(self._status.error) + )) + raise PushToProjectError(self._status.fail_reason) anatomy = Anatomy(src_project_name) @@ -499,10 +606,16 @@ class ProjectPushItemProcess: ProjectPushRepreItem(repre_doc, anatomy.roots) for repre_doc in repre_docs ] - self._status.add_progress_message(( + self._status.debug(( f"Found {len(repre_items)} representations on" f" version {src_version_id} in project '{src_project_name}'" )) + if not repre_items: + self._status.set_fail_reason( + "Source version does not have representations" + f" (Version id: {src_version_id})" + ) + raise PushToProjectError(self._status.fail_reason) self._src_anatomy = anatomy self._src_project_doc = project_doc @@ -517,12 +630,12 @@ class ProjectPushItemProcess: # Validate project existence dst_project_doc = get_project(dst_project_name) if not dst_project_doc: - self._status.error = ( + self._status.set_fail_reason( f"Destination project '{dst_project_name}' was not found" ) - raise PushToProjectError(self._status.error) + raise PushToProjectError(self._status.fail_reason) - self._status.add_progress_message( + self._status.debug( f"Destination project '{dst_project_name}' found" ) self._project_doc = dst_project_doc @@ -556,10 +669,10 @@ class ProjectPushItemProcess: for other_asset_doc in other_asset_docs: other_name = other_asset_doc["name"] if other_name.lower() == asset_name_low: - self._status.add_progress_message( + self._status.debug(( f"Found already existing asset with name \"{other_name}\"" f" which match requested name \"{asset_name}\"" - ) + )) return other_asset_doc data_keys = ( @@ -597,7 +710,7 @@ class ProjectPushItemProcess: asset_doc["type"], asset_doc ) - self._status.add_progress_message( + self._status.info( f"Creating new asset with name \"{asset_name}\"" ) self._created_asset_doc = asset_doc @@ -609,10 +722,10 @@ class ProjectPushItemProcess: dst_task_name = self._item.dst_task_name new_asset_name = self._item.new_asset_name if not dst_asset_id and not new_asset_name: - self._status.error = ( + self._status.set_fail_reason( "Push item does not have defined destination asset" ) - raise PushToProjectError(self._status.error) + raise PushToProjectError(self._status.fail_reason) # Get asset document parent_asset_doc = None @@ -621,11 +734,11 @@ class ProjectPushItemProcess: self._item.dst_project_name, self._item.dst_asset_id ) if not parent_asset_doc: - self._status.error = ( + self._status.set_fail_reason( f"Could find asset with id \"{dst_asset_id}\"" f" in project \"{dst_project_name}\"" ) - raise PushToProjectError(self._status.error) + raise PushToProjectError(self._status.fail_reason) if not new_asset_name: asset_doc = parent_asset_doc @@ -647,12 +760,12 @@ class ProjectPushItemProcess: asset_tasks = asset_doc.get("data", {}).get("tasks") or {} task_info = asset_tasks.get(dst_task_name) if not task_info: - self._status.error = ( + self._status.set_fail_reason( f"Could find task with name \"{dst_task_name}\"" f" on asset \"{asset_path}\"" f" in project \"{dst_project_name}\"" ) - raise PushToProjectError(self._status.error) + raise PushToProjectError(self._status.fail_reason) # Create copy of task info to avoid changing data in asset document task_info = copy.deepcopy(task_info) @@ -671,12 +784,12 @@ class ProjectPushItemProcess: family = families[0] if not family: - self._status.error = ( + self._status.set_fail_reason( "Couldn't figure out family from source subset" ) - raise PushToProjectError(self._status.error) + raise PushToProjectError(self._status.fail_reason) - self._status.add_progress_message( + self._status.debug( f"Publishing family is '{family}' (Based on source subset)" ) self._family = family @@ -690,7 +803,7 @@ class ProjectPushItemProcess: self.task_info.get("type"), project_settings=self.project_settings ) - self._status.add_progress_message( + self._status.debug( f"Using template '{template_name}' for integration" ) self._template_name = template_name @@ -708,8 +821,8 @@ class ProjectPushItemProcess: host_name=self.host_name, project_settings=self.project_settings ) - self._status.add_progress_message( - f"Push will be integrating to subet with name '{subset_name}'" + self._status.info( + f"Push will be integrating to subset with name '{subset_name}'" ) self._subset_name = subset_name @@ -733,20 +846,7 @@ class ProjectPushItemProcess: self._subset_doc = subset_doc def make_sure_version_exists(self): - """Make sure version document exits in database. - - Args: - item_process (ProjectPushItemProcess): Item handling process. - project_name (str): Name of project where version should live. - version (Union[int, None]): Number of version. Latest is used when - 'None' is passed. - operations (OperationsSession): Session which handler creation and - update of entities. - - Returns: - Tuple[Dict[str, Any], bool]: New version document and boolean if version - already existed in database. - """ + """Make sure version document exits in database.""" project_name = self._item.dst_project_name version = self._item.dst_version @@ -851,16 +951,19 @@ class ProjectPushItemProcess: file_template = StringTemplate( anatomy.templates[template_name]["file"] ) + self._status.info("Preparing files to transfer") processed_repre_items = self._prepare_file_transactions( anatomy, template_name, formatting_data, file_template ) self._file_transaction.process() + self._status.info("Preparing database changes") self._prepare_database_operations( version_id, processed_repre_items, path_template, existing_repres_by_low_name ) + self._status.info("Finalization") self._operations.commit() self._file_transaction.finalize() @@ -1005,15 +1108,33 @@ class ProjectPushItemProcess: ) def process(self): - item_process.fill_source_variables() - item_process.fill_destination_project() - item_process.fill_or_create_destination_asset() - item_process.determine_family() - item_process.determine_publish_template_name() - item_process.determine_subset_name() - item_process.make_sure_subset_exists() - item_process.make_sure_version_exists() - item_process.integrate_representations() + try: + self._status.info("Process started") + self.fill_source_variables() + self._status.info("Source entities were found") + self.fill_destination_project() + self._status.info("Destination project was found") + self.fill_or_create_destination_asset() + self._status.info("Destination asset was determined") + self.determine_family() + self.determine_publish_template_name() + self.determine_subset_name() + self.make_sure_subset_exists() + self.make_sure_version_exists() + self._status.info("Prerequirements were prepared") + self.integrate_representations() + self._status.info("Integration finished") + + except PushToProjectError: + pass + + except Exception as exc: + self._status.set_fail_reason( + "Unhandled error happened: {}".format(str(exc)) + ) + + finally: + self._status.set_finished() def main(): From 3eed7b8de0b8a0963d1eba13b704cece21e5429b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 5 Jan 2023 18:09:00 +0100 Subject: [PATCH 11/24] added app definition for push to project --- openpype/tools/push_to_project/app.py | 41 +++++++++++++++++++ .../push_to_project/control_integrate.py | 25 ----------- openpype/tools/push_to_project/window.py | 35 ---------------- 3 files changed, 41 insertions(+), 60 deletions(-) create mode 100644 openpype/tools/push_to_project/app.py diff --git a/openpype/tools/push_to_project/app.py b/openpype/tools/push_to_project/app.py new file mode 100644 index 0000000000..9ca5fd83e9 --- /dev/null +++ b/openpype/tools/push_to_project/app.py @@ -0,0 +1,41 @@ +import click +from qtpy import QtWidgets, QtCore + +from openpype.tools.push_to_project.window import PushToContextSelectWindow + + +@click.command() +@click.option("--project", help="Source project name") +@click.option("--version", help="Source version id") +def main(project, version): + """Run PushToProject tool to integrate version in different project. + + Args: + project (str): Source project name. + version (str): Version id. + """ + + app = QtWidgets.QApplication.instance() + if not app: + # 'AA_EnableHighDpiScaling' must be set before app instance creation + high_dpi_scale_attr = getattr( + QtCore.Qt, "AA_EnableHighDpiScaling", None + ) + if high_dpi_scale_attr is not None: + QtWidgets.QApplication.setAttribute(high_dpi_scale_attr) + + app = QtWidgets.QApplication([]) + + attr = getattr(QtCore.Qt, "AA_UseHighDpiPixmaps", None) + if attr is not None: + app.setAttribute(attr) + + window = PushToContextSelectWindow() + window.show() + window.controller.set_source(project, version) + + app.exec_() + + +if __name__ == "__main__": + main() diff --git a/openpype/tools/push_to_project/control_integrate.py b/openpype/tools/push_to_project/control_integrate.py index e4ae9decd3..fec9b9ddd2 100644 --- a/openpype/tools/push_to_project/control_integrate.py +++ b/openpype/tools/push_to_project/control_integrate.py @@ -1135,28 +1135,3 @@ class ProjectPushItemProcess: finally: self._status.set_finished() - - -def main(): - # NOTE For development purposes - project_name = "" - version_id = "" - dst_project_name = "" - dst_asset_id = "" - dst_task_name = "" - version = None - variant = "" - comment = "" - - item = ProjectPushItem( - project_name, - version_id, - dst_project_name, - dst_asset_id, - dst_task_name, - variant, - version, - dst_version=1 - ) - item_process = ProjectPushItemProcess(item) - item_process.process() diff --git a/openpype/tools/push_to_project/window.py b/openpype/tools/push_to_project/window.py index 99a77caa9e..a68f0f5340 100644 --- a/openpype/tools/push_to_project/window.py +++ b/openpype/tools/push_to_project/window.py @@ -705,38 +705,3 @@ class PushToContextSelectWindow(QtWidgets.QWidget): def _on_select_click(self): self._controller.submit() - - -def main(): - app = QtWidgets.QApplication.instance() - if not app: - # 'AA_EnableHighDpiScaling' must be set before app instance creation - high_dpi_scale_attr = getattr( - QtCore.Qt, "AA_EnableHighDpiScaling", None - ) - if high_dpi_scale_attr is not None: - QtWidgets.QApplication.setAttribute(high_dpi_scale_attr) - - app = QtWidgets.QApplication([]) - - for attr_name in ( - "AA_UseHighDpiPixmaps", - ): - attr = getattr(QtCore.Qt, attr_name, None) - if attr is not None: - app.setAttribute(attr) - - # TODO find way how to get these - project_name = None - version_id = None - - # Show window dialog - window = PushToContextSelectWindow() - window.controller.set_source(project_name, version_id) - window.show() - - app.exec_() - - -if __name__ == "__main__": - main() \ No newline at end of file From e2f3aadb45e076560c1f2a6b1d2d3db3524eee8f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 5 Jan 2023 18:09:16 +0100 Subject: [PATCH 12/24] added 'ValidatedLineEdit' to style --- openpype/style/style.css | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/style/style.css b/openpype/style/style.css index a7a48cdb9d..c96239dbd5 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -1416,6 +1416,13 @@ CreateNextPageOverlay { } /* Globally used names */ +#ValidatedLineEdit[state="valid"], #ValidatedLineEdit[state="valid"]:focus, #ValidatedLineEdit[state="valid"]:hover { + border-color: {color:publisher:success}; +} +#ValidatedLineEdit[state="invalid"], #ValidatedLineEdit[state="invalid"]:focus, #ValidatedLineEdit[state="invalid"]:hover { + border-color: {color:publisher:error}; +} + #Separator { background: {color:bg-menu-separator}; } From 74984afc7be1e0d707b0742fa594bc75396a8eee Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 5 Jan 2023 18:11:49 +0100 Subject: [PATCH 13/24] added new 'LoadError' to avoid showing traceback --- openpype/pipeline/load/__init__.py | 1 + openpype/pipeline/load/utils.py | 10 ++++++++ openpype/tools/loader/widgets.py | 38 ++++++++++++++++++------------ 3 files changed, 34 insertions(+), 15 deletions(-) diff --git a/openpype/pipeline/load/__init__.py b/openpype/pipeline/load/__init__.py index e96f64f2a4..8bd09876bf 100644 --- a/openpype/pipeline/load/__init__.py +++ b/openpype/pipeline/load/__init__.py @@ -1,6 +1,7 @@ from .utils import ( HeroVersionType, + LoadError, IncompatibleLoaderError, InvalidRepresentationContext, diff --git a/openpype/pipeline/load/utils.py b/openpype/pipeline/load/utils.py index 784d4628f3..e2b3675115 100644 --- a/openpype/pipeline/load/utils.py +++ b/openpype/pipeline/load/utils.py @@ -60,6 +60,16 @@ class HeroVersionType(object): return self.version.__format__(format_spec) +class LoadError(Exception): + """Known error that happened during loading. + + A message is shown to user (without traceback). Make sure an artist can + understand the problem. + """ + + pass + + class IncompatibleLoaderError(ValueError): """Error when Loader is incompatible with a representation.""" pass diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 826c7110da..b0b43c1e40 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -29,6 +29,7 @@ from openpype.pipeline.load import ( load_with_repre_context, load_with_subset_context, load_with_subset_contexts, + LoadError, IncompatibleLoaderError, ) from openpype.tools.utils import ( @@ -1577,6 +1578,7 @@ def _load_representations_by_loader(loader, repre_contexts, repre_context, options=options ) + except IncompatibleLoaderError as exc: print(exc) error_info.append(( @@ -1588,10 +1590,13 @@ def _load_representations_by_loader(loader, repre_contexts, )) except Exception as exc: - exc_type, exc_value, exc_traceback = sys.exc_info() - formatted_traceback = "".join(traceback.format_exception( - exc_type, exc_value, exc_traceback - )) + formatted_traceback = None + if not isinstance(exc, LoadError): + exc_type, exc_value, exc_traceback = sys.exc_info() + formatted_traceback = "".join(traceback.format_exception( + exc_type, exc_value, exc_traceback + )) + error_info.append(( str(exc), formatted_traceback, @@ -1616,7 +1621,7 @@ def _load_subsets_by_loader(loader, subset_contexts, options, error_info = [] if options is None: # not load when cancelled - return + return error_info if loader.is_multiple_contexts_compatible: subset_names = [] @@ -1631,13 +1636,14 @@ def _load_subsets_by_loader(loader, subset_contexts, options, subset_contexts, options=options ) + except Exception as exc: - exc_type, exc_value, exc_traceback = sys.exc_info() - formatted_traceback = "".join( - traceback.format_exception( + formatted_traceback = None + if not isinstance(exc, LoadError): + exc_type, exc_value, exc_traceback = sys.exc_info() + formatted_traceback = "".join(traceback.format_exception( exc_type, exc_value, exc_traceback - ) - ) + )) error_info.append(( str(exc), formatted_traceback, @@ -1657,13 +1663,15 @@ def _load_subsets_by_loader(loader, subset_contexts, options, subset_context, options=options ) + except Exception as exc: - exc_type, exc_value, exc_traceback = sys.exc_info() - formatted_traceback = "\n".join( - traceback.format_exception( + formatted_traceback = None + if not isinstance(exc, LoadError): + exc_type, exc_value, exc_traceback = sys.exc_info() + formatted_traceback = "".join(traceback.format_exception( exc_type, exc_value, exc_traceback - ) - ) + )) + error_info.append(( str(exc), formatted_traceback, From 23d2f78a3829c1095bfeeaf82fab4e1dafb44c3c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 5 Jan 2023 18:13:44 +0100 Subject: [PATCH 14/24] added push to library loader --- openpype/plugins/load/push_to_library.py | 52 ++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 openpype/plugins/load/push_to_library.py diff --git a/openpype/plugins/load/push_to_library.py b/openpype/plugins/load/push_to_library.py new file mode 100644 index 0000000000..dd7291e686 --- /dev/null +++ b/openpype/plugins/load/push_to_library.py @@ -0,0 +1,52 @@ +import os + +from openpype import PACKAGE_DIR +from openpype.lib import get_openpype_execute_args, run_detached_process +from openpype.pipeline import load +from openpype.pipeline.load import LoadError + + +class PushToLibraryProject(load.SubsetLoaderPlugin): + """Export selected versions to folder structure from Template""" + + is_multiple_contexts_compatible = True + + representations = ["*"] + families = ["*"] + + label = "Push to Library project" + order = 35 + icon = "send" + color = "#d8d8d8" + + def load(self, contexts, name=None, namespace=None, options=None): + filtered_contexts = [ + context + for context in contexts + if context.get("project") and context.get("version") + ] + 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( + PACKAGE_DIR, + "tools", + "push_to_project", + "app.py" + ) + project_doc = context["project"] + version_doc = context["version"] + project_name = project_doc["name"] + version_id = str(version_doc["_id"]) + + args = get_openpype_execute_args( + "run", + push_tool_script_path, + "--project", project_name, + "--version", version_id + ) + run_detached_process(args) From ae52289e13c52ca4e6646c470c5ed257683d2a6f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 5 Jan 2023 18:15:24 +0100 Subject: [PATCH 15/24] filter library projects --- openpype/tools/push_to_project/control_context.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/tools/push_to_project/control_context.py b/openpype/tools/push_to_project/control_context.py index 242ca475e7..bfa6042504 100644 --- a/openpype/tools/push_to_project/control_context.py +++ b/openpype/tools/push_to_project/control_context.py @@ -127,6 +127,9 @@ class EntitiesModel: project_names = [] project_docs_by_name = {} for project_doc in get_projects(): + library_project = project_doc["data"].get("library_project") + if not library_project: + continue project_name = project_doc["name"] project_names.append(project_name) project_docs_by_name[project_name] = project_doc From c74a969ae4e8f97bd7f597f62bb0dc188777f791 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 5 Jan 2023 19:26:31 +0100 Subject: [PATCH 16/24] added ability to submit integration --- .../tools/push_to_project/control_context.py | 55 ++++++++++++++++--- 1 file changed, 48 insertions(+), 7 deletions(-) diff --git a/openpype/tools/push_to_project/control_context.py b/openpype/tools/push_to_project/control_context.py index bfa6042504..aebdd5a0d0 100644 --- a/openpype/tools/push_to_project/control_context.py +++ b/openpype/tools/push_to_project/control_context.py @@ -1,5 +1,7 @@ import re +import time import collections +import threading from openpype.client import ( get_projects, @@ -17,6 +19,12 @@ from openpype.pipeline.create import ( get_subset_name_template, ) +from .control_integrate import ( + ProjectPushItem, + ProjectPushItemProcess, + ProjectPushItemStatus, +) + class AssetItem: def __init__( @@ -382,6 +390,7 @@ class PushToContextController: event_system.add_callback("new_asset_name.changed", self._invalidate) self._submission_enabled = False + self._process_thread = None self.set_source(project_name, version_id) @@ -620,12 +629,44 @@ class PushToContextController: return asset_item.name return None - def submit(self): + def submit(self, wait=True): if not self.submission_enabled: return - project_name = self.selection_model.project_name - asset_id = self.selection_model.asset_id - task_name = self.selection_model.task_name - self.dst_project_name = project_name - self.dst_asset_id = asset_id - self.dst_task_name = task_name + + if self._process_thread is not None: + return + + self._event_system.emit("submit.started", {}, "controller") + thread = threading.Thread(target=self._submit_callback) + thread.start() + if wait: + while thread.is_alive(): + time.sleep(0.1) + thread.join() + self._event_system.emit("submit.finished", {}, "controller") + return + self._process_thread = thread + + def wait_for_process_thread(self): + if self._process_thread is None: + return + self._process_thread.join() + self._process_thread = None + + def _submit_callback(self): + item = ProjectPushItem( + self.src_project_name, + self.src_version_id, + self.selection_model.project_name, + self.selection_model.asset_id, + self.selection_model.task_name, + self.user_values.variant, + comment=self.user_values.comment, + new_asset_name=self.user_values.new_asset_name, + dst_version=1 + ) + + status_item = ProjectPushItemStatus(event_system=self._event_system) + process_item = ProjectPushItemProcess(item, status_item) + process_item.process() + self._event_system.emit("submit.finished", {}, "controller") From ad99133cf06d2a0a344a53ddcb9429f020ecf942 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 5 Jan 2023 19:36:49 +0100 Subject: [PATCH 17/24] added base of pushing --- .../push_to_project/control_integrate.py | 51 +++++-- openpype/tools/push_to_project/window.py | 131 ++++++++++++++++-- 2 files changed, 162 insertions(+), 20 deletions(-) diff --git a/openpype/tools/push_to_project/control_integrate.py b/openpype/tools/push_to_project/control_integrate.py index fec9b9ddd2..7ad2e1e874 100644 --- a/openpype/tools/push_to_project/control_integrate.py +++ b/openpype/tools/push_to_project/control_integrate.py @@ -4,6 +4,8 @@ import copy import socket import itertools import datetime +import sys +import traceback from bson.objectid import ObjectId @@ -143,7 +145,8 @@ class ProjectPushItemStatus: failed=False, finished=False, fail_reason=None, - messages=None + messages=None, + event_system=None ): if messages is None: messages = [] @@ -151,6 +154,13 @@ class ProjectPushItemStatus: self._finished = finished self._fail_reason = fail_reason self._messages = messages + self._event_system = event_system + + def emit_event(self, topic, data=None): + if self._event_system is None: + return + + self._event_system.emit(topic, data or {}, "push.status") def get_finished(self): """Processing of push to project finished. @@ -170,6 +180,7 @@ class ProjectPushItemStatus: if finished != self._finished: self._finished = finished + self.emit_event("push.finished.changed", {"finished": finished}) def get_failed(self): """Processing failed. @@ -192,7 +203,10 @@ class ProjectPushItemStatus: fail_reason (str): Reason why failed. """ - self._failed = failed + if self._failed != failed: + self._failed = failed + self.emit_event("push.failed.changed", {"failed": failed}) + if fail_reason is not UNKNOWN and self._fail_reason != fail_reason: self.set_fail_reason(fail_reason) @@ -223,6 +237,7 @@ class ProjectPushItemStatus: if self._fail_reason == reason: return self._fail_reason = reason + self.emit_event("push.fail_reason.changed", {"fail_reason": reason}) if reason and not self._failed: self.set_failed(True) @@ -238,6 +253,10 @@ class ProjectPushItemStatus: def add_message(self, message, level): message_obj = StatusMessage(message, level) self._messages.append(message_obj) + self.emit_event( + "push.message.added", + {"message": message, "level": level} + ) print(message_obj) return message_obj @@ -664,16 +683,26 @@ class ProjectPushItemProcess: asset_name_low = asset_name.lower() other_asset_docs = get_assets( - project_doc["name"], parent_ids=[parent_id], fields=["name"] + project_doc["name"], fields=["_id", "name", "data.visualParent"] ) for other_asset_doc in other_asset_docs: other_name = other_asset_doc["name"] - if other_name.lower() == asset_name_low: - self._status.debug(( - f"Found already existing asset with name \"{other_name}\"" - f" which match requested name \"{asset_name}\"" + other_parent_id = other_asset_doc["data"].get("visualParent") + if other_name.lower() != asset_name_low: + continue + + if other_parent_id != parent_id: + self._status.set_fail_reason(( + f"Asset with name \"{other_name}\" already" + " exists in different hierarchy." )) - return other_asset_doc + raise PushToProjectError(self._status.fail_reason) + + self._status.debug(( + f"Found already existing asset with name \"{other_name}\"" + f" which match requested name \"{asset_name}\"" + )) + return get_asset_by_id(project_doc["name"], other_asset_doc["_id"]) data_keys = ( "clipIn", @@ -1129,8 +1158,12 @@ class ProjectPushItemProcess: pass except Exception as exc: + _exc, _value, _tb = sys.exc_info() self._status.set_fail_reason( - "Unhandled error happened: {}".format(str(exc)) + "Unhandled error happened: {}\n{}".format( + str(exc), + "".join(traceback.format_exception(_exc, _value, _tb)) + ) ) finally: diff --git a/openpype/tools/push_to_project/window.py b/openpype/tools/push_to_project/window.py index a68f0f5340..5dea0401cb 100644 --- a/openpype/tools/push_to_project/window.py +++ b/openpype/tools/push_to_project/window.py @@ -373,7 +373,9 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self.setWindowTitle("Push to project (select context)") self.setWindowIcon(QtGui.QIcon(get_app_icon_path())) - header_widget = QtWidgets.QWidget(self) + main_context_widget = QtWidgets.QWidget(self) + + header_widget = QtWidgets.QWidget(main_context_widget) header_label = QtWidgets.QLabel(controller.src_label, header_widget) @@ -381,7 +383,9 @@ class PushToContextSelectWindow(QtWidgets.QWidget): header_layout.setContentsMargins(0, 0, 0, 0) header_layout.addWidget(header_label) - main_splitter = QtWidgets.QSplitter(QtCore.Qt.Horizontal, self) + main_splitter = QtWidgets.QSplitter( + QtCore.Qt.Horizontal, main_context_widget + ) context_widget = QtWidgets.QWidget(main_splitter) @@ -455,22 +459,57 @@ class PushToContextSelectWindow(QtWidgets.QWidget): btns_layout.addWidget(cancel_btn, 0) btns_layout.addWidget(publish_btn, 0) - sep_1 = SeparatorWidget(parent=self) - sep_2 = SeparatorWidget(parent=self) - main_layout = QtWidgets.QVBoxLayout(self) - main_layout.addWidget(header_widget, 0) - main_layout.addWidget(sep_1, 0) - main_layout.addWidget(main_splitter, 1) - main_layout.addWidget(sep_2, 0) - main_layout.addWidget(btns_widget, 0) + sep_1 = SeparatorWidget(parent=main_context_widget) + sep_2 = SeparatorWidget(parent=main_context_widget) + main_context_layout = QtWidgets.QVBoxLayout(main_context_widget) + main_context_layout.addWidget(header_widget, 0) + main_context_layout.addWidget(sep_1, 0) + main_context_layout.addWidget(main_splitter, 1) + main_context_layout.addWidget(sep_2, 0) + main_context_layout.addWidget(btns_widget, 0) + + # NOTE This was added in hurry + # - should be reorganized and changed styles + overlay_widget = QtWidgets.QFrame(self) + overlay_widget.setObjectName("OverlayFrame") + + overlay_label = QtWidgets.QLabel(overlay_widget) + overlay_label.setAlignment(QtCore.Qt.AlignCenter) + + overlay_btns_widget = QtWidgets.QWidget(overlay_widget) + overlay_btns_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + # Add try again button (requires changes in controller) + overlay_close_btn = QtWidgets.QPushButton("Close", overlay_btns_widget) + + overlay_btns_layout = QtWidgets.QHBoxLayout(overlay_btns_widget) + overlay_btns_layout.addStretch(1) + overlay_btns_layout.addWidget(overlay_close_btn, 0) + overlay_btns_layout.addStretch(1) + + overlay_layout = QtWidgets.QVBoxLayout(overlay_widget) + overlay_layout.addWidget(overlay_label, 0) + overlay_layout.addWidget(overlay_btns_widget, 0) + overlay_layout.setAlignment(QtCore.Qt.AlignCenter) + + main_layout = QtWidgets.QStackedLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(main_context_widget) + main_layout.addWidget(overlay_widget) + main_layout.setStackingMode(QtWidgets.QStackedLayout.StackAll) + main_layout.setCurrentWidget(main_context_widget) show_timer = QtCore.QTimer() show_timer.setInterval(1) + main_thread_timer = QtCore.QTimer() + main_thread_timer.setInterval(10) + user_input_changed_timer = QtCore.QTimer() user_input_changed_timer.setInterval(200) user_input_changed_timer.setSingleShot(True) + main_thread_timer.timeout.connect(self._on_main_thread_timer) show_timer.timeout.connect(self._on_show_timer) user_input_changed_timer.timeout.connect(self._on_user_input_timer) asset_name_input.textChanged.connect(self._on_new_asset_change) @@ -488,6 +527,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget): task_model.items_changed.connect(self._on_task_model_change) publish_btn.clicked.connect(self._on_select_click) cancel_btn.clicked.connect(self._on_close_click) + overlay_close_btn.clicked.connect(self._on_close_click) controller.event_system.add_callback( "new_asset_name.changed", self._on_controller_new_asset_change @@ -504,6 +544,25 @@ class PushToContextSelectWindow(QtWidgets.QWidget): controller.event_system.add_callback( "source.changed", self._on_controller_source_change ) + controller.event_system.add_callback( + "submit.started", self._on_controller_submit_start + ) + controller.event_system.add_callback( + "submit.finished", self._on_controller_submit_end + ) + controller.event_system.add_callback( + "push.failed.changed", self._on_push_failed + ) + controller.event_system.add_callback( + "push.fail_reason.changed", self._on_push_failed_reason + ) + controller.event_system.add_callback( + "push.message.added", self._on_push_message + ) + + self._main_layout = main_layout + + self._main_context_widget = main_context_widget self._header_label = header_label self._main_splitter = main_splitter @@ -526,6 +585,10 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self._publish_btn = publish_btn + self._overlay_widget = overlay_widget + self._overlay_close_btn = overlay_close_btn + self._overlay_label = overlay_label + self._user_input_changed_timer = user_input_changed_timer # Store current value on input text change # The value is unset when is passed to controller @@ -538,7 +601,15 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self._show_counter = 2 self._first_show = True + self._main_thread_timer = main_thread_timer + self._main_thread_timer_can_stop = True + self._submit_messages = [] + self._last_submit_message = None + self._push_fail_reason = "Unknown" + self._push_failed = False + publish_btn.setEnabled(False) + overlay_close_btn.setVisible(False) if controller.user_values.new_asset_name: asset_name_input.setText(controller.user_values.new_asset_name) @@ -704,4 +775,42 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self.close() def _on_select_click(self): - self._controller.submit() + self._controller.submit(wait=False) + + def _on_main_thread_timer(self): + if self._last_submit_message: + self._overlay_label.setText(self._last_submit_message) + self._last_submit_message = None + + if self._main_thread_timer_can_stop: + self._main_thread_timer.stop() + self._overlay_close_btn.setVisible(True) + + if self._push_failed: + self._overlay_label.setText( + "Push Failed\n{}".format(self._push_fail_reason) + ) + set_style_property( + self._overlay_close_btn, + "state", + "error" + ) + + def _on_controller_submit_start(self): + 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") + + def _on_controller_submit_end(self): + self._main_thread_timer_can_stop = True + + def _on_push_message(self, event): + self._submit_messages.append(event["message"]) + self._last_submit_message = event["message"] + + def _on_push_failed_reason(self, event): + self._push_fail_reason = event["fail_reason"] + + def _on_push_failed(self, event): + self._push_failed = event["failed"] From 75fefd6ae2eb8af58d02f5e4a7201ae5a154cb15 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 5 Jan 2023 19:37:12 +0100 Subject: [PATCH 18/24] added errored button --- openpype/style/style.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/style/style.css b/openpype/style/style.css index c96239dbd5..da477eeefa 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -148,6 +148,10 @@ QPushButton::menu-indicator { padding-right: 5px; } +QPushButton[state="error"] { + background: {color:publisher:error}; +} + QToolButton { border: 0px solid transparent; background: {color:bg-buttons}; From 23817bdc4cf1394a78af62a2532d0a8c62d0dbb5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 6 Jan 2023 12:17:46 +0100 Subject: [PATCH 19/24] formatting changes --- openpype/tools/push_to_project/control_integrate.py | 5 ++++- openpype/tools/push_to_project/window.py | 1 - 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/tools/push_to_project/control_integrate.py b/openpype/tools/push_to_project/control_integrate.py index 7ad2e1e874..522d0f7a31 100644 --- a/openpype/tools/push_to_project/control_integrate.py +++ b/openpype/tools/push_to_project/control_integrate.py @@ -409,7 +409,10 @@ class ProjectPushRepreItem: filepath_template = file_info["path"].replace("\\", "/") filepath = filepath_template.format(root=self._roots) dirpath, basename = os.path.split(filepath_template) - if dirpath != src_dirpath or not src_basename_regex.match(basename): + if ( + dirpath != src_dirpath + or not src_basename_regex.match(basename) + ): relative_dir = dirpath.replace(src_dirpath, "") if relative_dir: relative_path = "/".join([relative_dir, basename]) diff --git a/openpype/tools/push_to_project/window.py b/openpype/tools/push_to_project/window.py index 5dea0401cb..c43ecad160 100644 --- a/openpype/tools/push_to_project/window.py +++ b/openpype/tools/push_to_project/window.py @@ -1,4 +1,3 @@ -import re import collections from qtpy import QtWidgets, QtGui, QtCore From c1e5daa35934e5cd1588a7db14259b117792c617 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 6 Jan 2023 12:18:00 +0100 Subject: [PATCH 20/24] use 'DeselectableTreeView' for asset view --- openpype/tools/push_to_project/window.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/tools/push_to_project/window.py b/openpype/tools/push_to_project/window.py index c43ecad160..963e519b12 100644 --- a/openpype/tools/push_to_project/window.py +++ b/openpype/tools/push_to_project/window.py @@ -9,6 +9,7 @@ from openpype.tools.utils import ( get_asset_icon_by_name, set_style_property, ) +from openpype.tools.utils.views import DeselectableTreeView from .control_context import PushToContextController @@ -401,7 +402,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget): QtCore.Qt.Vertical, context_widget ) - asset_view = QtWidgets.QTreeView(asset_task_splitter) + asset_view = DeselectableTreeView(asset_task_splitter) asset_view.setHeaderHidden(True) asset_model = AssetsModel(controller) asset_proxy = QtCore.QSortFilterProxyModel() From 6266877bc660725f30e7f1dab0544f94e5d34760 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 6 Jan 2023 13:38:02 +0100 Subject: [PATCH 21/24] cleanup the processing a little bit --- .../tools/push_to_project/control_context.py | 43 ++++--- .../push_to_project/control_integrate.py | 121 ++++++++++-------- openpype/tools/push_to_project/window.py | 64 +++++---- 3 files changed, 128 insertions(+), 100 deletions(-) diff --git a/openpype/tools/push_to_project/control_context.py b/openpype/tools/push_to_project/control_context.py index aebdd5a0d0..a45fab618a 100644 --- a/openpype/tools/push_to_project/control_context.py +++ b/openpype/tools/push_to_project/control_context.py @@ -391,6 +391,7 @@ class PushToContextController: self._submission_enabled = False self._process_thread = None + self._process_item = None self.set_source(project_name, version_id) @@ -636,24 +637,6 @@ class PushToContextController: if self._process_thread is not None: return - self._event_system.emit("submit.started", {}, "controller") - thread = threading.Thread(target=self._submit_callback) - thread.start() - if wait: - while thread.is_alive(): - time.sleep(0.1) - thread.join() - self._event_system.emit("submit.finished", {}, "controller") - return - self._process_thread = thread - - def wait_for_process_thread(self): - if self._process_thread is None: - return - self._process_thread.join() - self._process_thread = None - - def _submit_callback(self): item = ProjectPushItem( self.src_project_name, self.src_version_id, @@ -668,5 +651,29 @@ class PushToContextController: status_item = ProjectPushItemStatus(event_system=self._event_system) process_item = ProjectPushItemProcess(item, status_item) + self._process_item = process_item + self._event_system.emit("submit.started", {}, "controller") + if wait: + self._submit_callback() + self._process_item = None + return process_item + + thread = threading.Thread(target=self._submit_callback) + self._process_thread = thread + thread.start() + return process_item + + def wait_for_process_thread(self): + if self._process_thread is None: + return + self._process_thread.join() + self._process_thread = None + + def _submit_callback(self): + process_item = self._process_item + if process_item is None: + return process_item.process() self._event_system.emit("submit.finished", {}, "controller") + if process_item is self._process_item: + self._process_item = None diff --git a/openpype/tools/push_to_project/control_integrate.py b/openpype/tools/push_to_project/control_integrate.py index 522d0f7a31..704ed7ba51 100644 --- a/openpype/tools/push_to_project/control_integrate.py +++ b/openpype/tools/push_to_project/control_integrate.py @@ -145,6 +145,7 @@ class ProjectPushItemStatus: failed=False, finished=False, fail_reason=None, + formatted_traceback=None, messages=None, event_system=None ): @@ -153,6 +154,7 @@ class ProjectPushItemStatus: self._failed = failed self._finished = finished self._fail_reason = fail_reason + self._traceback = formatted_traceback self._messages = messages self._event_system = event_system @@ -182,16 +184,9 @@ class ProjectPushItemStatus: self._finished = finished self.emit_event("push.finished.changed", {"finished": finished}) - def get_failed(self): - """Processing failed. + finished = property(get_finished, set_finished) - Returns: - bool: Processing failed. - """ - - return self._failed - - def set_failed(self, failed, fail_reason=UNKNOWN): + def set_failed(self, fail_reason, exc_info=None): """Set status as failed. Attribute 'fail_reason' can change automatically based on passed value. @@ -203,20 +198,48 @@ class ProjectPushItemStatus: fail_reason (str): Reason why failed. """ - if self._failed != failed: - self._failed = failed - self.emit_event("push.failed.changed", {"failed": failed}) + failed = True + if not fail_reason and not exc_info: + failed = False - if fail_reason is not UNKNOWN and self._fail_reason != fail_reason: - self.set_fail_reason(fail_reason) + full_traceback = None + if exc_info is not None: + full_traceback = "".join(traceback.format_exception(*exc_info)) + if not fail_reason: + fail_reason = "Failed without specified reason" - if failed and self._fail_reason is None: - self.set_fail_reason("Failed without specified reason") + if ( + self._failed == failed + and self._traceback == full_traceback + and self._fail_reason == fail_reason + ): + return - elif not failed and self._fail_reason: - self.set_fail_reason(None) + self._failed = failed + self._fail_reason = fail_reason or None + self._traceback = full_traceback - def get_fail_reason(self): + self.emit_event( + "push.failed.changed", + { + "failed": failed, + "reason": fail_reason, + "traceback": full_traceback + } + ) + + @property + def failed(self): + """Processing failed. + + Returns: + bool: Processing failed. + """ + + return self._failed + + @property + def fail_reason(self): """Reason why push to process failed. Returns: @@ -225,28 +248,17 @@ class ProjectPushItemStatus: return self._fail_reason - def set_fail_reason(self, reason): - """Mark process status as failed. + @property + def traceback(self): + """Traceback of failed process. - Status is also set to failed if 'reason' is not None. + Traceback is available only if unhandled exception happened. - Args: - reason (str): Reason why push to project failed. + Returns: + Union[str, None]: Formatted traceback. """ - if self._fail_reason == reason: - return - self._fail_reason = reason - self.emit_event("push.fail_reason.changed", {"fail_reason": reason}) - if reason and not self._failed: - self.set_failed(True) - - if reason: - print(f"Integration failed: {reason}") - - finished = property(get_finished, set_finished) - failed = property(get_failed, set_failed) - fail_reason = property(get_fail_reason, set_fail_reason) + return self._traceback # Loggin helpers # TODO better logging @@ -585,7 +597,7 @@ class ProjectPushItemProcess: project_doc = get_project(src_project_name) if not project_doc: - self._status.set_fail_reason( + self._status.set_failed( f"Source project \"{src_project_name}\" was not found" ) raise PushToProjectError(self._status.fail_reason) @@ -594,7 +606,7 @@ class ProjectPushItemProcess: version_doc = get_version_by_id(src_project_name, src_version_id) if not version_doc: - self._status.set_fail_reason(( + self._status.set_failed(( f"Source version with id \"{src_version_id}\"" f" was not found in project \"{src_project_name}\"" )) @@ -603,7 +615,7 @@ class ProjectPushItemProcess: subset_id = version_doc["parent"] subset_doc = get_subset_by_id(src_project_name, subset_id) if not subset_doc: - self._status.set_fail_reason(( + self._status.set_failed(( f"Could find subset with id \"{subset_id}\"" f" in project \"{src_project_name}\"" )) @@ -612,7 +624,7 @@ class ProjectPushItemProcess: asset_id = subset_doc["parent"] asset_doc = get_asset_by_id(src_project_name, asset_id) if not asset_doc: - self._status.set_fail_reason(( + self._status.set_failed(( f"Could find asset with id \"{asset_id}\"" f" in project \"{src_project_name}\"" )) @@ -633,7 +645,7 @@ class ProjectPushItemProcess: f" version {src_version_id} in project '{src_project_name}'" )) if not repre_items: - self._status.set_fail_reason( + self._status.set_failed( "Source version does not have representations" f" (Version id: {src_version_id})" ) @@ -652,7 +664,7 @@ class ProjectPushItemProcess: # Validate project existence dst_project_doc = get_project(dst_project_name) if not dst_project_doc: - self._status.set_fail_reason( + self._status.set_failed( f"Destination project '{dst_project_name}' was not found" ) raise PushToProjectError(self._status.fail_reason) @@ -695,7 +707,7 @@ class ProjectPushItemProcess: continue if other_parent_id != parent_id: - self._status.set_fail_reason(( + self._status.set_failed(( f"Asset with name \"{other_name}\" already" " exists in different hierarchy." )) @@ -754,7 +766,7 @@ class ProjectPushItemProcess: dst_task_name = self._item.dst_task_name new_asset_name = self._item.new_asset_name if not dst_asset_id and not new_asset_name: - self._status.set_fail_reason( + self._status.set_failed( "Push item does not have defined destination asset" ) raise PushToProjectError(self._status.fail_reason) @@ -766,7 +778,7 @@ class ProjectPushItemProcess: self._item.dst_project_name, self._item.dst_asset_id ) if not parent_asset_doc: - self._status.set_fail_reason( + self._status.set_failed( f"Could find asset with id \"{dst_asset_id}\"" f" in project \"{dst_project_name}\"" ) @@ -792,7 +804,7 @@ class ProjectPushItemProcess: asset_tasks = asset_doc.get("data", {}).get("tasks") or {} task_info = asset_tasks.get(dst_task_name) if not task_info: - self._status.set_fail_reason( + self._status.set_failed( f"Could find task with name \"{dst_task_name}\"" f" on asset \"{asset_path}\"" f" in project \"{dst_project_name}\"" @@ -816,7 +828,7 @@ class ProjectPushItemProcess: family = families[0] if not family: - self._status.set_fail_reason( + self._status.set_failed( "Couldn't figure out family from source subset" ) raise PushToProjectError(self._status.fail_reason) @@ -1157,16 +1169,15 @@ class ProjectPushItemProcess: self.integrate_representations() self._status.info("Integration finished") - except PushToProjectError: - pass + except PushToProjectError as exc: + if not self._status.failed: + self._status.set_failed(str(exc)) except Exception as exc: _exc, _value, _tb = sys.exc_info() - self._status.set_fail_reason( - "Unhandled error happened: {}\n{}".format( - str(exc), - "".join(traceback.format_exception(_exc, _value, _tb)) - ) + self._status.set_failed( + "Unhandled error happened: {}".format(str(exc)), + (_exc, _value, _tb) ) finally: diff --git a/openpype/tools/push_to_project/window.py b/openpype/tools/push_to_project/window.py index 963e519b12..e62650ec53 100644 --- a/openpype/tools/push_to_project/window.py +++ b/openpype/tools/push_to_project/window.py @@ -480,10 +480,16 @@ class PushToContextSelectWindow(QtWidgets.QWidget): overlay_btns_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) # Add try again button (requires changes in controller) - overlay_close_btn = QtWidgets.QPushButton("Close", overlay_btns_widget) + overlay_try_btn = QtWidgets.QPushButton( + "Try again", overlay_btns_widget + ) + overlay_close_btn = QtWidgets.QPushButton( + "Close", overlay_btns_widget + ) overlay_btns_layout = QtWidgets.QHBoxLayout(overlay_btns_widget) overlay_btns_layout.addStretch(1) + overlay_btns_layout.addWidget(overlay_try_btn, 0) overlay_btns_layout.addWidget(overlay_close_btn, 0) overlay_btns_layout.addStretch(1) @@ -528,6 +534,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget): publish_btn.clicked.connect(self._on_select_click) cancel_btn.clicked.connect(self._on_close_click) overlay_close_btn.clicked.connect(self._on_close_click) + overlay_try_btn.clicked.connect(self._on_try_again_click) controller.event_system.add_callback( "new_asset_name.changed", self._on_controller_new_asset_change @@ -550,12 +557,6 @@ class PushToContextSelectWindow(QtWidgets.QWidget): controller.event_system.add_callback( "submit.finished", self._on_controller_submit_end ) - controller.event_system.add_callback( - "push.failed.changed", self._on_push_failed - ) - controller.event_system.add_callback( - "push.fail_reason.changed", self._on_push_failed_reason - ) controller.event_system.add_callback( "push.message.added", self._on_push_message ) @@ -587,6 +588,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self._overlay_widget = overlay_widget self._overlay_close_btn = overlay_close_btn + self._overlay_try_btn = overlay_try_btn self._overlay_label = overlay_label self._user_input_changed_timer = user_input_changed_timer @@ -603,13 +605,12 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self._main_thread_timer = main_thread_timer self._main_thread_timer_can_stop = True - self._submit_messages = [] self._last_submit_message = None - self._push_fail_reason = "Unknown" - self._push_failed = False + self._process_item = None publish_btn.setEnabled(False) overlay_close_btn.setVisible(False) + overlay_try_btn.setVisible(False) if controller.user_values.new_asset_name: asset_name_input.setText(controller.user_values.new_asset_name) @@ -775,26 +776,42 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self.close() def _on_select_click(self): - self._controller.submit(wait=False) + self._process_item = self._controller.submit(wait=False) + + def _on_try_again_click(self): + self._process_item = None + self._last_submit_message = None + + self._overlay_close_btn.setVisible(False) + self._overlay_try_btn.setVisible(False) + self._main_layout.setCurrentWidget(self._main_context_widget) def _on_main_thread_timer(self): if self._last_submit_message: self._overlay_label.setText(self._last_submit_message) self._last_submit_message = None + process_status = self._process_item.status + push_failed = process_status.failed + fail_traceback = process_status.traceback if self._main_thread_timer_can_stop: self._main_thread_timer.stop() self._overlay_close_btn.setVisible(True) + if push_failed and not fail_traceback: + self._overlay_try_btn.setVisible(True) - if self._push_failed: - self._overlay_label.setText( - "Push Failed\n{}".format(self._push_fail_reason) - ) - set_style_property( - self._overlay_close_btn, - "state", - "error" - ) + if push_failed: + message = "Push Failed:\n{}".format(process_status.fail_reason) + if fail_traceback: + message += "\n{}".format(fail_traceback) + self._overlay_label.setText(message) + set_style_property(self._overlay_close_btn, "state", "error") + + if self._main_thread_timer_can_stop: + # Join thread in controller + self._controller.wait_for_process_thread() + # Reset process item to None + self._process_item = None def _on_controller_submit_start(self): self._main_thread_timer_can_stop = False @@ -806,11 +823,4 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self._main_thread_timer_can_stop = True def _on_push_message(self, event): - self._submit_messages.append(event["message"]) self._last_submit_message = event["message"] - - def _on_push_failed_reason(self, event): - self._push_fail_reason = event["fail_reason"] - - def _on_push_failed(self, event): - self._push_failed = event["failed"] From 2742a366175f5ef7f996c546ecf566652ff1c095 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 6 Jan 2023 13:39:28 +0100 Subject: [PATCH 22/24] remove unused import --- openpype/tools/push_to_project/control_context.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/tools/push_to_project/control_context.py b/openpype/tools/push_to_project/control_context.py index a45fab618a..02f1da6733 100644 --- a/openpype/tools/push_to_project/control_context.py +++ b/openpype/tools/push_to_project/control_context.py @@ -1,5 +1,4 @@ import re -import time import collections import threading From bcbf43479a459687ca0fc5178af31bcb310a4141 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 17 Jan 2023 14:16:09 +0100 Subject: [PATCH 23/24] fix key in event data --- openpype/tools/push_to_project/control_context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/push_to_project/control_context.py b/openpype/tools/push_to_project/control_context.py index 02f1da6733..e4058893d5 100644 --- a/openpype/tools/push_to_project/control_context.py +++ b/openpype/tools/push_to_project/control_context.py @@ -356,7 +356,7 @@ class UserPublishValues: self._event_system.emit( "comment.changed", { - "new_asset_name": comment, + "comment": comment, "changes": { "comment": {"new": comment, "old": old_comment} } From 9b1b3a600ac33effcf01d27ab66877b768c32615 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 17 Jan 2023 14:41:34 +0100 Subject: [PATCH 24/24] use comment when is filled --- openpype/tools/push_to_project/control_integrate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/push_to_project/control_integrate.py b/openpype/tools/push_to_project/control_integrate.py index 704ed7ba51..819724ad4c 100644 --- a/openpype/tools/push_to_project/control_integrate.py +++ b/openpype/tools/push_to_project/control_integrate.py @@ -907,7 +907,7 @@ class ProjectPushItemProcess: "fps": src_data.get("fps"), "source": src_data.get("source"), "machine": socket.gethostname(), - "comment": "", + "comment": self._item.comment or "", "author": get_openpype_username(), "time": get_formatted_current_time(), }