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():
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/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)
diff --git a/openpype/style/style.css b/openpype/style/style.css
index a7a48cdb9d..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};
@@ -1416,6 +1420,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};
}
diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py
index b1619ca02b..faef6c8a26 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 (
@@ -1581,6 +1582,7 @@ def _load_representations_by_loader(loader, repre_contexts,
repre_context,
options=options
)
+
except IncompatibleLoaderError as exc:
print(exc)
error_info.append((
@@ -1592,10 +1594,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,
@@ -1620,7 +1625,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 = []
@@ -1635,13 +1640,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,
@@ -1661,13 +1667,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,
diff --git a/openpype/tools/push_to_project/__init__.py b/openpype/tools/push_to_project/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
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_context.py b/openpype/tools/push_to_project/control_context.py
new file mode 100644
index 0000000000..e4058893d5
--- /dev/null
+++ b/openpype/tools/push_to_project/control_context.py
@@ -0,0 +1,678 @@
+import re
+import collections
+import threading
+
+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,
+)
+
+from .control_integrate import (
+ ProjectPushItem,
+ ProjectPushItemProcess,
+ ProjectPushItemStatus,
+)
+
+
+class AssetItem:
+ def __init__(
+ self,
+ entity_id,
+ name,
+ icon_name,
+ icon_color,
+ parent_id,
+ has_children
+ ):
+ self.id = entity_id
+ self.name = name
+ 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, has_children=True):
+ 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"),
+ asset_doc["data"].get("color"),
+ parent_id,
+ has_children
+ )
+
+
+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_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 []
+
+ 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)
+ if not asset_task_items:
+ return []
+ 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():
+ 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
+ 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]
+ asset_docs_by_parent_id = collections.defaultdict(list)
+ for asset_doc in get_assets(project_name):
+ 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(
+ "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 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",
+ {
+ "comment": 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._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._process_thread = None
+ self._process_item = None
+
+ 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):
+ return self._event_system
+
+ @property
+ def model(self):
+ return self._entities_model
+
+ @property
+ 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, wait=True):
+ if not self.submission_enabled:
+ return
+
+ if self._process_thread is not None:
+ return
+
+ 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)
+ 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
new file mode 100644
index 0000000000..819724ad4c
--- /dev/null
+++ b/openpype/tools/push_to_project/control_integrate.py
@@ -0,0 +1,1184 @@
+import os
+import re
+import copy
+import socket
+import itertools
+import datetime
+import sys
+import traceback
+
+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,
+ get_version_by_id,
+ get_last_version_by_subset_id,
+ get_version_by_name,
+ get_representations,
+)
+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
+from openpype.pipeline.publish import get_publish_template_name
+from openpype.pipeline.create import get_subset_name
+
+UNKNOWN = object()
+
+
+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,
+ src_project_name,
+ src_version_id,
+ 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
+ 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.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,
+ str(dst_asset_id),
+ str(new_asset_name),
+ str(dst_task_name),
+ str(dst_version)
+ ])
+
+ @property
+ def id(self):
+ return self._id
+
+ def __repr__(self):
+ 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,
+ fail_reason=None,
+ formatted_traceback=None,
+ messages=None,
+ event_system=None
+ ):
+ if messages is None:
+ messages = []
+ self._failed = failed
+ self._finished = finished
+ self._fail_reason = fail_reason
+ self._traceback = formatted_traceback
+ 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.
+
+ Returns:
+ bool: Finished.
+ """
+
+ return 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
+ self.emit_event("push.finished.changed", {"finished": finished})
+
+ finished = property(get_finished, set_finished)
+
+ def set_failed(self, fail_reason, exc_info=None):
+ """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.
+ """
+
+ failed = True
+ if not fail_reason and not exc_info:
+ failed = False
+
+ 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 (
+ self._failed == failed
+ and self._traceback == full_traceback
+ and self._fail_reason == fail_reason
+ ):
+ return
+
+ self._failed = failed
+ self._fail_reason = fail_reason or None
+ self._traceback = full_traceback
+
+ 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:
+ Union[str, None]: Reason why push failed or None.
+ """
+
+ return self._fail_reason
+
+ @property
+ def traceback(self):
+ """Traceback of failed process.
+
+ Traceback is available only if unhandled exception happened.
+
+ Returns:
+ Union[str, None]: Formatted traceback.
+ """
+
+ return self._traceback
+
+ # Loggin helpers
+ # TODO better logging
+ 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
+
+ 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).
+ """
+
+ def __init__(self, repre_doc, roots):
+ self._repre_doc = repre_doc
+ self._roots = roots
+ self._src_files = None
+ self._resource_files = None
+ self._frame = UNKNOWN
+
+ @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
+
+ @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:
+ 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:
+ 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"]
+ # 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:
+ 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.
+ item_status (ProjectPushItemStatus): Object to store status.
+ """
+
+ # TODO where to get host?!!!
+ host_name = "republisher"
+
+ def __init__(self, item, item_status=None):
+ self._item = item
+
+ 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 = None
+ self._anatomy = None
+ self._asset_doc = None
+ self._created_asset_doc = None
+ self._task_info = None
+ self._subset_doc = None
+ self._version_doc = None
+
+ self._family = None
+ self._subset_name = None
+
+ self._project_settings = None
+ self._template_name = None
+
+ 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
+
+ @property
+ def src_anatomy(self):
+ return self._src_anatomy
+
+ @property
+ def src_asset_doc(self):
+ return self._src_asset_doc
+
+ @property
+ def src_subset_doc(self):
+ return self._src_subset_doc
+
+ @property
+ def src_version_doc(self):
+ return self._src_version_doc
+
+ @property
+ def src_repre_items(self):
+ return self._src_repre_items
+
+ @property
+ def project_doc(self):
+ return self._project_doc
+
+ @property
+ def anatomy(self):
+ return self._anatomy
+
+ @property
+ def project_settings(self):
+ return self._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):
+ return self._family
+
+ @property
+ def subset_name(self):
+ return self._subset_name
+
+ @property
+ def template_name(self):
+ 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
+
+ project_doc = get_project(src_project_name)
+ if not project_doc:
+ self._status.set_failed(
+ f"Source project \"{src_project_name}\" was not found"
+ )
+ raise PushToProjectError(self._status.fail_reason)
+
+ 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.set_failed((
+ f"Source version with id \"{src_version_id}\""
+ f" was not found in project \"{src_project_name}\""
+ ))
+ 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.set_failed((
+ f"Could find subset with id \"{subset_id}\""
+ f" in project \"{src_project_name}\""
+ ))
+ 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.set_failed((
+ f"Could find asset with id \"{asset_id}\""
+ f" in project \"{src_project_name}\""
+ ))
+ raise PushToProjectError(self._status.fail_reason)
+
+ anatomy = Anatomy(src_project_name)
+
+ repre_docs = get_representations(
+ src_project_name,
+ version_ids=[src_version_id]
+ )
+ repre_items = [
+ ProjectPushRepreItem(repre_doc, anatomy.roots)
+ for repre_doc in repre_docs
+ ]
+ 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_failed(
+ "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
+ 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.set_failed(
+ f"Destination project '{dst_project_name}' was not found"
+ )
+ raise PushToProjectError(self._status.fail_reason)
+
+ self._status.debug(
+ f"Destination project '{dst_project_name}' found"
+ )
+ self._project_doc = dst_project_doc
+ self._anatomy = Anatomy(dst_project_name)
+ self._project_settings = get_project_settings(
+ self._item.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"], fields=["_id", "name", "data.visualParent"]
+ )
+ for other_asset_doc in other_asset_docs:
+ other_name = other_asset_doc["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_failed((
+ f"Asset with name \"{other_name}\" already"
+ " exists in different hierarchy."
+ ))
+ 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",
+ "clipOut",
+ "frameStart",
+ "frameEnd",
+ "handleStart",
+ "handleEnd",
+ "resolutionWidth",
+ "resolutionHeight",
+ "fps",
+ "pixelAspect",
+ )
+ 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]
+
+ 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.info(
+ f"Creating new asset with name \"{asset_name}\""
+ )
+ self._created_asset_doc = asset_doc
+ return asset_doc
+
+ 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.set_failed(
+ "Push item does not have defined destination asset"
+ )
+ raise PushToProjectError(self._status.fail_reason)
+
+ # 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.set_failed(
+ f"Could find asset with id \"{dst_asset_id}\""
+ f" in project \"{dst_project_name}\""
+ )
+ raise PushToProjectError(self._status.fail_reason)
+
+ if not new_asset_name:
+ asset_doc = parent_asset_doc
+ else:
+ 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.set_failed(
+ f"Could find task with name \"{dst_task_name}\""
+ f" on asset \"{asset_path}\""
+ f" in project \"{dst_project_name}\""
+ )
+ raise PushToProjectError(self._status.fail_reason)
+
+ # 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.set_failed(
+ "Couldn't figure out family from source subset"
+ )
+ raise PushToProjectError(self._status.fail_reason)
+
+ self._status.debug(
+ 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.debug(
+ 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.info(
+ f"Push will be integrating to subset 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."""
+
+ 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": self._item.comment or "",
+ "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"]
+ )
+ 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()
+
+ 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):
+ 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 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_failed(
+ "Unhandled error happened: {}".format(str(exc)),
+ (_exc, _value, _tb)
+ )
+
+ finally:
+ self._status.set_finished()
diff --git a/openpype/tools/push_to_project/window.py b/openpype/tools/push_to_project/window.py
new file mode 100644
index 0000000000..e62650ec53
--- /dev/null
+++ b/openpype/tools/push_to_project/window.py
@@ -0,0 +1,826 @@
+import collections
+
+from qtpy import QtWidgets, QtGui, QtCore
+
+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 openpype.tools.utils.views import DeselectableTreeView
+
+from .control_context import PushToContextController
+
+PROJECT_NAME_ROLE = QtCore.Qt.UserRole + 1
+ASSET_NAME_ROLE = QtCore.Qt.UserRole + 2
+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):
+ empty_text = "< Empty >"
+ refreshing_text = "< Refreshing >"
+ select_project_text = "< Select Project >"
+
+ refreshed = QtCore.Signal()
+
+ def __init__(self, controller):
+ super(ProjectsModel, self).__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.takeRow(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)
+ self.refreshed.emit()
+
+
+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):
+ items_changed = QtCore.Signal()
+ empty_text = "< Empty >"
+
+ def __init__(self, controller):
+ super(AssetsModel, 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.started", self._on_refresh_start
+ )
+ self.event_system.add_callback(
+ "assets.refresh.finished", self._on_refresh_finish
+ )
+
+ 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)
+ 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):
+ project_name = event["project_name"]
+ if project_name == self._last_project:
+ return
+
+ self._last_project = project_name
+ self._clear()
+ self.items_changed.emit()
+
+ 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
+ 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()
+ 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)
+ 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()
+ item.setFlags(
+ QtCore.Qt.ItemIsSelectable
+ | QtCore.Qt.ItemIsEnabled
+ )
+ new_items.append(item)
+ self._items[asset_item.id] = item
+
+ 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)
+
+ hierarchy_queue.append((asset_item.id, item))
+
+ 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.takeRow(item.row())
+
+ self.items_changed.emit()
+
+
+class TasksModel(QtGui.QStandardItemModel):
+ items_changed = QtCore.Signal()
+ 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()
+ self.items_changed.emit()
+
+ 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()
+ self.items_changed.emit()
+ 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()
+ self.items_changed.emit()
+ 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())
+
+ self.items_changed.emit()
+
+
+class PushToContextSelectWindow(QtWidgets.QWidget):
+ def __init__(self, controller=None):
+ super(PushToContextSelectWindow, self).__init__()
+ if controller is None:
+ controller = PushToContextController()
+ self._controller = controller
+
+ self.setWindowTitle("Push to project (select context)")
+ self.setWindowIcon(QtGui.QIcon(get_app_icon_path()))
+
+ main_context_widget = QtWidgets.QWidget(self)
+
+ header_widget = QtWidgets.QWidget(main_context_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, main_context_widget
+ )
+
+ 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_task_splitter = QtWidgets.QSplitter(
+ QtCore.Qt.Vertical, context_widget
+ )
+
+ asset_view = DeselectableTreeView(asset_task_splitter)
+ asset_view.setHeaderHidden(True)
+ asset_model = AssetsModel(controller)
+ asset_proxy = QtCore.QSortFilterProxyModel()
+ asset_proxy.setSourceModel(asset_model)
+ asset_proxy.setDynamicSortFilter(True)
+ asset_view.setModel(asset_proxy)
+
+ 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)
+
+ asset_task_splitter.addWidget(asset_view)
+ asset_task_splitter.addWidget(task_view)
+
+ 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)
+ 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(cancel_btn, 0)
+ btns_layout.addWidget(publish_btn, 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_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)
+
+ 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)
+ 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
+ )
+ 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)
+ 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
+ )
+ 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
+ )
+ 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.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
+
+ 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._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._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
+ # 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
+
+ self._main_thread_timer = main_thread_timer
+ self._main_thread_timer_can_stop = True
+ self._last_submit_message = None
+ 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)
+ 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(PushToContextSelectWindow, self).showEvent(event)
+ if self._first_show:
+ self._first_show = False
+ 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()
+ 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_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)
+ 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_submission_change(self, event):
+ self._publish_btn.setEnabled(event["enabled"])
+
+ def _on_close_click(self):
+ self.close()
+
+ def _on_select_click(self):
+ 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 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
+ 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._last_submit_message = event["message"]
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):