mirror of
https://github.com/ynput/ayon-core.git
synced 2026-01-01 16:34:53 +01:00
Merge branch 'develop' into bugfix/AY-5716_Hiero-loader-with-old-expected-data
This commit is contained in:
commit
e10df3725c
1489 changed files with 10109 additions and 5756 deletions
|
|
@ -8,7 +8,6 @@ import inspect
|
||||||
import logging
|
import logging
|
||||||
import threading
|
import threading
|
||||||
import collections
|
import collections
|
||||||
|
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from abc import ABCMeta, abstractmethod
|
from abc import ABCMeta, abstractmethod
|
||||||
|
|
||||||
|
|
@ -51,8 +50,25 @@ IGNORED_MODULES_IN_AYON = set()
|
||||||
# - this is used to log the missing addon
|
# - this is used to log the missing addon
|
||||||
MOVED_ADDON_MILESTONE_VERSIONS = {
|
MOVED_ADDON_MILESTONE_VERSIONS = {
|
||||||
"applications": VersionInfo(0, 2, 0),
|
"applications": VersionInfo(0, 2, 0),
|
||||||
|
"blender": VersionInfo(0, 2, 0),
|
||||||
|
"celaction": VersionInfo(0, 2, 0),
|
||||||
|
"clockify": VersionInfo(0, 2, 0),
|
||||||
|
"flame": VersionInfo(0, 2, 0),
|
||||||
|
"fusion": VersionInfo(0, 2, 0),
|
||||||
|
"harmony": VersionInfo(0, 2, 0),
|
||||||
|
"hiero": VersionInfo(0, 2, 0),
|
||||||
|
"max": VersionInfo(0, 2, 0),
|
||||||
|
"photoshop": VersionInfo(0, 2, 0),
|
||||||
|
"traypublisher": VersionInfo(0, 2, 0),
|
||||||
|
"tvpaint": VersionInfo(0, 2, 0),
|
||||||
|
"maya": VersionInfo(0, 2, 0),
|
||||||
|
"nuke": VersionInfo(0, 2, 0),
|
||||||
|
"resolve": VersionInfo(0, 2, 0),
|
||||||
|
"substancepainter": VersionInfo(0, 2, 0),
|
||||||
|
"houdini": VersionInfo(0, 3, 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Inherit from `object` for Python 2 hosts
|
# Inherit from `object` for Python 2 hosts
|
||||||
class _ModuleClass(object):
|
class _ModuleClass(object):
|
||||||
"""Fake module class for storing AYON addons.
|
"""Fake module class for storing AYON addons.
|
||||||
|
|
@ -538,6 +554,9 @@ class AYONAddon(object):
|
||||||
enabled = True
|
enabled = True
|
||||||
_id = None
|
_id = None
|
||||||
|
|
||||||
|
# Temporary variable for 'version' property
|
||||||
|
_missing_version_warned = False
|
||||||
|
|
||||||
def __init__(self, manager, settings):
|
def __init__(self, manager, settings):
|
||||||
self.manager = manager
|
self.manager = manager
|
||||||
|
|
||||||
|
|
@ -568,6 +587,26 @@ class AYONAddon(object):
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def version(self):
|
||||||
|
"""Addon version.
|
||||||
|
|
||||||
|
Todo:
|
||||||
|
Should be abstract property (required). Introduced in
|
||||||
|
ayon-core 0.3.3 .
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Addon version as semver compatible string.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not self.__class__._missing_version_warned:
|
||||||
|
self.__class__._missing_version_warned = True
|
||||||
|
print(
|
||||||
|
f"DEV WARNING: Addon '{self.name}' does not have"
|
||||||
|
f" defined version."
|
||||||
|
)
|
||||||
|
return "0.0.0"
|
||||||
|
|
||||||
def initialize(self, settings):
|
def initialize(self, settings):
|
||||||
"""Initialization of addon attributes.
|
"""Initialization of addon attributes.
|
||||||
|
|
||||||
|
|
@ -683,6 +722,30 @@ class OpenPypeAddOn(OpenPypeModule):
|
||||||
enabled = True
|
enabled = True
|
||||||
|
|
||||||
|
|
||||||
|
class _AddonReportInfo:
|
||||||
|
def __init__(
|
||||||
|
self, class_name, name, version, report_value_by_label
|
||||||
|
):
|
||||||
|
self.class_name = class_name
|
||||||
|
self.name = name
|
||||||
|
self.version = version
|
||||||
|
self.report_value_by_label = report_value_by_label
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_addon(cls, addon, report):
|
||||||
|
class_name = addon.__class__.__name__
|
||||||
|
report_value_by_label = {
|
||||||
|
label: reported.get(class_name)
|
||||||
|
for label, reported in report.items()
|
||||||
|
}
|
||||||
|
return cls(
|
||||||
|
addon.__class__.__name__,
|
||||||
|
addon.name,
|
||||||
|
addon.version,
|
||||||
|
report_value_by_label
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AddonsManager:
|
class AddonsManager:
|
||||||
"""Manager of addons that helps to load and prepare them to work.
|
"""Manager of addons that helps to load and prepare them to work.
|
||||||
|
|
||||||
|
|
@ -859,10 +922,6 @@ class AddonsManager:
|
||||||
name_alias = getattr(addon, "openpype_alias", None)
|
name_alias = getattr(addon, "openpype_alias", None)
|
||||||
if name_alias:
|
if name_alias:
|
||||||
aliased_names.append((name_alias, addon))
|
aliased_names.append((name_alias, addon))
|
||||||
enabled_str = "X"
|
|
||||||
if not addon.enabled:
|
|
||||||
enabled_str = " "
|
|
||||||
self.log.debug("[{}] {}".format(enabled_str, name))
|
|
||||||
|
|
||||||
now = time.time()
|
now = time.time()
|
||||||
report[addon.__class__.__name__] = now - prev_start_time
|
report[addon.__class__.__name__] = now - prev_start_time
|
||||||
|
|
@ -874,6 +933,13 @@ class AddonsManager:
|
||||||
exc_info=True
|
exc_info=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
for addon_name in sorted(self._addons_by_name.keys()):
|
||||||
|
addon = self._addons_by_name[addon_name]
|
||||||
|
enabled_str = "X" if addon.enabled else " "
|
||||||
|
self.log.debug(
|
||||||
|
f"[{enabled_str}] {addon.name} ({addon.version})"
|
||||||
|
)
|
||||||
|
|
||||||
for item in aliased_names:
|
for item in aliased_names:
|
||||||
name_alias, addon = item
|
name_alias, addon = item
|
||||||
if name_alias not in self._addons_by_name:
|
if name_alias not in self._addons_by_name:
|
||||||
|
|
@ -1162,39 +1228,55 @@ class AddonsManager:
|
||||||
available_col_names |= set(addon_names.keys())
|
available_col_names |= set(addon_names.keys())
|
||||||
|
|
||||||
# Prepare ordered dictionary for columns
|
# Prepare ordered dictionary for columns
|
||||||
cols = collections.OrderedDict()
|
addons_info = [
|
||||||
# Add addon names to first columnt
|
_AddonReportInfo.from_addon(addon, self._report)
|
||||||
cols["Addon name"] = list(sorted(
|
|
||||||
addon.__class__.__name__
|
|
||||||
for addon in self.addons
|
for addon in self.addons
|
||||||
if addon.__class__.__name__ in available_col_names
|
if addon.__class__.__name__ in available_col_names
|
||||||
))
|
]
|
||||||
|
addons_info.sort(key=lambda x: x.name)
|
||||||
|
|
||||||
|
addon_name_rows = [
|
||||||
|
addon_info.name
|
||||||
|
for addon_info in addons_info
|
||||||
|
]
|
||||||
|
addon_version_rows = [
|
||||||
|
addon_info.version
|
||||||
|
for addon_info in addons_info
|
||||||
|
]
|
||||||
|
|
||||||
# Add total key (as last addon)
|
# Add total key (as last addon)
|
||||||
cols["Addon name"].append(self._report_total_key)
|
addon_name_rows.append(self._report_total_key)
|
||||||
|
addon_version_rows.append(f"({len(addons_info)})")
|
||||||
|
|
||||||
|
cols = collections.OrderedDict()
|
||||||
|
# Add addon names to first columnt
|
||||||
|
cols["Addon name"] = addon_name_rows
|
||||||
|
cols["Version"] = addon_version_rows
|
||||||
|
|
||||||
# Add columns from report
|
# Add columns from report
|
||||||
|
total_by_addon = {
|
||||||
|
row: 0
|
||||||
|
for row in addon_name_rows
|
||||||
|
}
|
||||||
for label in self._report.keys():
|
for label in self._report.keys():
|
||||||
cols[label] = []
|
rows = []
|
||||||
|
col_total = 0
|
||||||
total_addon_times = {}
|
for addon_info in addons_info:
|
||||||
for addon_name in cols["Addon name"]:
|
value = addon_info.report_value_by_label.get(label)
|
||||||
total_addon_times[addon_name] = 0
|
if value is None:
|
||||||
|
rows.append("N/A")
|
||||||
for label, reported in self._report.items():
|
|
||||||
for addon_name in cols["Addon name"]:
|
|
||||||
col_time = reported.get(addon_name)
|
|
||||||
if col_time is None:
|
|
||||||
cols[label].append("N/A")
|
|
||||||
continue
|
continue
|
||||||
cols[label].append("{:.3f}".format(col_time))
|
rows.append("{:.3f}".format(value))
|
||||||
total_addon_times[addon_name] += col_time
|
total_by_addon[addon_info.name] += value
|
||||||
|
col_total += value
|
||||||
|
total_by_addon[self._report_total_key] += col_total
|
||||||
|
rows.append("{:.3f}".format(col_total))
|
||||||
|
cols[label] = rows
|
||||||
# Add to also total column that should sum the row
|
# Add to also total column that should sum the row
|
||||||
cols[self._report_total_key] = []
|
cols[self._report_total_key] = [
|
||||||
for addon_name in cols["Addon name"]:
|
"{:.3f}".format(total_by_addon[addon_name])
|
||||||
cols[self._report_total_key].append(
|
for addon_name in cols["Addon name"]
|
||||||
"{:.3f}".format(total_addon_times[addon_name])
|
]
|
||||||
)
|
|
||||||
|
|
||||||
# Prepare column widths and total row count
|
# Prepare column widths and total row count
|
||||||
# - column width is by
|
# - column width is by
|
||||||
|
|
@ -1321,7 +1403,7 @@ class TrayAddonsManager(AddonsManager):
|
||||||
self.doubleclick_callback = None
|
self.doubleclick_callback = None
|
||||||
|
|
||||||
def add_doubleclick_callback(self, addon, callback):
|
def add_doubleclick_callback(self, addon, callback):
|
||||||
"""Register doubleclick callbacks on tray icon.
|
"""Register double-click callbacks on tray icon.
|
||||||
|
|
||||||
Currently, there is no way how to determine which is launched. Name of
|
Currently, there is no way how to determine which is launched. Name of
|
||||||
callback can be defined with `doubleclick_callback` attribute.
|
callback can be defined with `doubleclick_callback` attribute.
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
from ayon_applications import PreLaunchHook
|
from ayon_applications import PreLaunchHook
|
||||||
|
|
||||||
from ayon_core.pipeline.colorspace import get_imageio_config
|
from ayon_core.pipeline.colorspace import get_imageio_config_preset
|
||||||
from ayon_core.pipeline.template_data import get_template_data_with_names
|
from ayon_core.pipeline.template_data import get_template_data
|
||||||
|
|
||||||
|
|
||||||
class OCIOEnvHook(PreLaunchHook):
|
class OCIOEnvHook(PreLaunchHook):
|
||||||
|
|
@ -26,32 +26,38 @@ class OCIOEnvHook(PreLaunchHook):
|
||||||
def execute(self):
|
def execute(self):
|
||||||
"""Hook entry method."""
|
"""Hook entry method."""
|
||||||
|
|
||||||
template_data = get_template_data_with_names(
|
folder_entity = self.data["folder_entity"]
|
||||||
project_name=self.data["project_name"],
|
|
||||||
folder_path=self.data["folder_path"],
|
template_data = get_template_data(
|
||||||
task_name=self.data["task_name"],
|
self.data["project_entity"],
|
||||||
|
folder_entity=folder_entity,
|
||||||
|
task_entity=self.data["task_entity"],
|
||||||
host_name=self.host_name,
|
host_name=self.host_name,
|
||||||
settings=self.data["project_settings"]
|
settings=self.data["project_settings"],
|
||||||
)
|
)
|
||||||
|
|
||||||
config_data = get_imageio_config(
|
config_data = get_imageio_config_preset(
|
||||||
project_name=self.data["project_name"],
|
self.data["project_name"],
|
||||||
host_name=self.host_name,
|
self.data["folder_path"],
|
||||||
project_settings=self.data["project_settings"],
|
self.data["task_name"],
|
||||||
anatomy_data=template_data,
|
self.host_name,
|
||||||
anatomy=self.data["anatomy"],
|
anatomy=self.data["anatomy"],
|
||||||
|
project_settings=self.data["project_settings"],
|
||||||
|
template_data=template_data,
|
||||||
env=self.launch_context.env,
|
env=self.launch_context.env,
|
||||||
|
folder_id=folder_entity["id"],
|
||||||
)
|
)
|
||||||
|
|
||||||
if config_data:
|
if not config_data:
|
||||||
ocio_path = config_data["path"]
|
|
||||||
|
|
||||||
if self.host_name in ["nuke", "hiero"]:
|
|
||||||
ocio_path = ocio_path.replace("\\", "/")
|
|
||||||
|
|
||||||
self.log.info(
|
|
||||||
f"Setting OCIO environment to config path: {ocio_path}")
|
|
||||||
|
|
||||||
self.launch_context.env["OCIO"] = ocio_path
|
|
||||||
else:
|
|
||||||
self.log.debug("OCIO not set or enabled")
|
self.log.debug("OCIO not set or enabled")
|
||||||
|
return
|
||||||
|
|
||||||
|
ocio_path = config_data["path"]
|
||||||
|
|
||||||
|
if self.host_name in ["nuke", "hiero"]:
|
||||||
|
ocio_path = ocio_path.replace("\\", "/")
|
||||||
|
|
||||||
|
self.log.info(
|
||||||
|
f"Setting OCIO environment to config path: {ocio_path}")
|
||||||
|
|
||||||
|
self.launch_context.env["OCIO"] = ocio_path
|
||||||
|
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
from .addon import BlenderAddon
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = (
|
|
||||||
"BlenderAddon",
|
|
||||||
)
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
from .addon import (
|
|
||||||
HOST_DIR,
|
|
||||||
FlameAddon,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = (
|
|
||||||
"HOST_DIR",
|
|
||||||
"FlameAddon",
|
|
||||||
)
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
from .addon import (
|
|
||||||
HIERO_ROOT_DIR,
|
|
||||||
HieroAddon,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = (
|
|
||||||
"HIERO_ROOT_DIR",
|
|
||||||
"HieroAddon",
|
|
||||||
)
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
"""Collector for pointcache types.
|
|
||||||
|
|
||||||
This will add additional family to pointcache instance based on
|
|
||||||
the creator_identifier parameter.
|
|
||||||
"""
|
|
||||||
import pyblish.api
|
|
||||||
|
|
||||||
|
|
||||||
class CollectPointcacheType(pyblish.api.InstancePlugin):
|
|
||||||
"""Collect data type for pointcache instance."""
|
|
||||||
|
|
||||||
order = pyblish.api.CollectorOrder
|
|
||||||
hosts = ["houdini"]
|
|
||||||
families = ["pointcache"]
|
|
||||||
label = "Collect type of pointcache"
|
|
||||||
|
|
||||||
def process(self, instance):
|
|
||||||
if instance.data["creator_identifier"] == "io.openpype.creators.houdini.bgeo": # noqa: E501
|
|
||||||
instance.data["families"] += ["bgeo"]
|
|
||||||
elif instance.data["creator_identifier"] == "io.openpype.creators.houdini.pointcache": # noqa: E501
|
|
||||||
instance.data["families"] += ["abc"]
|
|
||||||
|
|
@ -1,71 +0,0 @@
|
||||||
import pyblish.api
|
|
||||||
import ayon_core.hosts.maya.api.action
|
|
||||||
from ayon_core.pipeline.publish import (
|
|
||||||
PublishValidationError,
|
|
||||||
ValidateContentsOrder,
|
|
||||||
OptionalPyblishPluginMixin
|
|
||||||
)
|
|
||||||
from maya import cmds
|
|
||||||
|
|
||||||
|
|
||||||
class ValidateAnimatedReferenceRig(pyblish.api.InstancePlugin,
|
|
||||||
OptionalPyblishPluginMixin):
|
|
||||||
"""Validate all nodes in skeletonAnim_SET are referenced"""
|
|
||||||
|
|
||||||
order = ValidateContentsOrder
|
|
||||||
hosts = ["maya"]
|
|
||||||
families = ["animation.fbx"]
|
|
||||||
label = "Animated Reference Rig"
|
|
||||||
accepted_controllers = ["transform", "locator"]
|
|
||||||
actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction]
|
|
||||||
optional = False
|
|
||||||
|
|
||||||
def process(self, instance):
|
|
||||||
if not self.is_active(instance.data):
|
|
||||||
return
|
|
||||||
animated_sets = instance.data.get("animated_skeleton", [])
|
|
||||||
if not animated_sets:
|
|
||||||
self.log.debug(
|
|
||||||
"No nodes found in skeletonAnim_SET. "
|
|
||||||
"Skipping validation of animated reference rig..."
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
for animated_reference in animated_sets:
|
|
||||||
is_referenced = cmds.referenceQuery(
|
|
||||||
animated_reference, isNodeReferenced=True)
|
|
||||||
if not bool(is_referenced):
|
|
||||||
raise PublishValidationError(
|
|
||||||
"All the content in skeletonAnim_SET"
|
|
||||||
" should be referenced nodes"
|
|
||||||
)
|
|
||||||
invalid_controls = self.validate_controls(animated_sets)
|
|
||||||
if invalid_controls:
|
|
||||||
raise PublishValidationError(
|
|
||||||
"All the content in skeletonAnim_SET"
|
|
||||||
" should be transforms"
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def validate_controls(self, set_members):
|
|
||||||
"""Check if the controller set contains only accepted node types.
|
|
||||||
|
|
||||||
Checks if all its set members are within the hierarchy of the root
|
|
||||||
Checks if the node types of the set members valid
|
|
||||||
|
|
||||||
Args:
|
|
||||||
set_members: list of nodes of the skeleton_anim_set
|
|
||||||
hierarchy: list of nodes which reside under the root node
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
errors (list)
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Validate control types
|
|
||||||
invalid = []
|
|
||||||
set_members = cmds.ls(set_members, long=True)
|
|
||||||
for node in set_members:
|
|
||||||
if cmds.nodeType(node) not in self.accepted_controllers:
|
|
||||||
invalid.append(node)
|
|
||||||
|
|
||||||
return invalid
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
from .addon import ResolveAddon
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = (
|
|
||||||
"ResolveAddon",
|
|
||||||
)
|
|
||||||
|
|
@ -80,17 +80,21 @@ def get_engine_versions(env=None):
|
||||||
def get_editor_exe_path(engine_path: Path, engine_version: str) -> Path:
|
def get_editor_exe_path(engine_path: Path, engine_version: str) -> Path:
|
||||||
"""Get UE Editor executable path."""
|
"""Get UE Editor executable path."""
|
||||||
ue_path = engine_path / "Engine/Binaries"
|
ue_path = engine_path / "Engine/Binaries"
|
||||||
|
|
||||||
|
ue_name = "UnrealEditor"
|
||||||
|
|
||||||
|
# handle older versions of Unreal Engine
|
||||||
|
if engine_version.split(".")[0] == "4":
|
||||||
|
ue_name = "UE4Editor"
|
||||||
|
|
||||||
if platform.system().lower() == "windows":
|
if platform.system().lower() == "windows":
|
||||||
if engine_version.split(".")[0] == "4":
|
ue_path /= f"Win64/{ue_name}.exe"
|
||||||
ue_path /= "Win64/UE4Editor.exe"
|
|
||||||
elif engine_version.split(".")[0] == "5":
|
|
||||||
ue_path /= "Win64/UnrealEditor.exe"
|
|
||||||
|
|
||||||
elif platform.system().lower() == "linux":
|
elif platform.system().lower() == "linux":
|
||||||
ue_path /= "Linux/UE4Editor"
|
ue_path /= f"Linux/{ue_name}"
|
||||||
|
|
||||||
elif platform.system().lower() == "darwin":
|
elif platform.system().lower() == "darwin":
|
||||||
ue_path /= "Mac/UE4Editor"
|
ue_path /= f"Mac/{ue_name}"
|
||||||
|
|
||||||
return ue_path
|
return ue_path
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
from .clockify_module import ClockifyModule
|
|
||||||
|
|
||||||
__all__ = (
|
|
||||||
"ClockifyModule",
|
|
||||||
)
|
|
||||||
|
|
@ -29,15 +29,11 @@ from ayon_core.pipeline.publish.lib import (
|
||||||
JSONDecodeError = getattr(json.decoder, "JSONDecodeError", ValueError)
|
JSONDecodeError = getattr(json.decoder, "JSONDecodeError", ValueError)
|
||||||
|
|
||||||
|
|
||||||
# TODO both 'requests_post' and 'requests_get' should not set 'verify' based
|
|
||||||
# on environment variable. This should be done in a more controlled way,
|
|
||||||
# e.g. each deadline url could have checkbox to enabled/disable
|
|
||||||
# ssl verification.
|
|
||||||
def requests_post(*args, **kwargs):
|
def requests_post(*args, **kwargs):
|
||||||
"""Wrap request post method.
|
"""Wrap request post method.
|
||||||
|
|
||||||
Disabling SSL certificate validation if ``DONT_VERIFY_SSL`` environment
|
Disabling SSL certificate validation if ``verify`` kwarg is set to False.
|
||||||
variable is found. This is useful when Deadline server is
|
This is useful when Deadline server is
|
||||||
running with self-signed certificates and its certificate is not
|
running with self-signed certificates and its certificate is not
|
||||||
added to trusted certificates on client machines.
|
added to trusted certificates on client machines.
|
||||||
|
|
||||||
|
|
@ -46,10 +42,6 @@ def requests_post(*args, **kwargs):
|
||||||
of defense SSL is providing, and it is not recommended.
|
of defense SSL is providing, and it is not recommended.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if 'verify' not in kwargs:
|
|
||||||
kwargs['verify'] = False if os.getenv("OPENPYPE_DONT_VERIFY_SSL",
|
|
||||||
True) else True # noqa
|
|
||||||
|
|
||||||
auth = kwargs.get("auth")
|
auth = kwargs.get("auth")
|
||||||
if auth:
|
if auth:
|
||||||
kwargs["auth"] = tuple(auth) # explicit cast to tuple
|
kwargs["auth"] = tuple(auth) # explicit cast to tuple
|
||||||
|
|
@ -61,8 +53,8 @@ def requests_post(*args, **kwargs):
|
||||||
def requests_get(*args, **kwargs):
|
def requests_get(*args, **kwargs):
|
||||||
"""Wrap request get method.
|
"""Wrap request get method.
|
||||||
|
|
||||||
Disabling SSL certificate validation if ``DONT_VERIFY_SSL`` environment
|
Disabling SSL certificate validation if ``verify`` kwarg is set to False.
|
||||||
variable is found. This is useful when Deadline server is
|
This is useful when Deadline server is
|
||||||
running with self-signed certificates and its certificate is not
|
running with self-signed certificates and its certificate is not
|
||||||
added to trusted certificates on client machines.
|
added to trusted certificates on client machines.
|
||||||
|
|
||||||
|
|
@ -71,9 +63,6 @@ def requests_get(*args, **kwargs):
|
||||||
of defense SSL is providing, and it is not recommended.
|
of defense SSL is providing, and it is not recommended.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if 'verify' not in kwargs:
|
|
||||||
kwargs['verify'] = False if os.getenv("OPENPYPE_DONT_VERIFY_SSL",
|
|
||||||
True) else True # noqa
|
|
||||||
auth = kwargs.get("auth")
|
auth = kwargs.get("auth")
|
||||||
if auth:
|
if auth:
|
||||||
kwargs["auth"] = tuple(auth)
|
kwargs["auth"] = tuple(auth)
|
||||||
|
|
@ -466,7 +455,8 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin,
|
||||||
self.aux_files = self.get_aux_files()
|
self.aux_files = self.get_aux_files()
|
||||||
|
|
||||||
auth = instance.data["deadline"]["auth"]
|
auth = instance.data["deadline"]["auth"]
|
||||||
job_id = self.process_submission(auth)
|
verify = instance.data["deadline"]["verify"]
|
||||||
|
job_id = self.process_submission(auth, verify)
|
||||||
self.log.info("Submitted job to Deadline: {}.".format(job_id))
|
self.log.info("Submitted job to Deadline: {}.".format(job_id))
|
||||||
|
|
||||||
# TODO: Find a way that's more generic and not render type specific
|
# TODO: Find a way that's more generic and not render type specific
|
||||||
|
|
@ -479,10 +469,10 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin,
|
||||||
job_info=render_job_info,
|
job_info=render_job_info,
|
||||||
plugin_info=render_plugin_info
|
plugin_info=render_plugin_info
|
||||||
)
|
)
|
||||||
render_job_id = self.submit(payload, auth)
|
render_job_id = self.submit(payload, auth, verify)
|
||||||
self.log.info("Render job id: %s", render_job_id)
|
self.log.info("Render job id: %s", render_job_id)
|
||||||
|
|
||||||
def process_submission(self, auth=None):
|
def process_submission(self, auth=None, verify=True):
|
||||||
"""Process data for submission.
|
"""Process data for submission.
|
||||||
|
|
||||||
This takes Deadline JobInfo, PluginInfo, AuxFile, creates payload
|
This takes Deadline JobInfo, PluginInfo, AuxFile, creates payload
|
||||||
|
|
@ -493,7 +483,7 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin,
|
||||||
|
|
||||||
"""
|
"""
|
||||||
payload = self.assemble_payload()
|
payload = self.assemble_payload()
|
||||||
return self.submit(payload, auth)
|
return self.submit(payload, auth, verify)
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_job_info(self):
|
def get_job_info(self):
|
||||||
|
|
@ -583,7 +573,7 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin,
|
||||||
"AuxFiles": aux_files or self.aux_files
|
"AuxFiles": aux_files or self.aux_files
|
||||||
}
|
}
|
||||||
|
|
||||||
def submit(self, payload, auth):
|
def submit(self, payload, auth, verify):
|
||||||
"""Submit payload to Deadline API end-point.
|
"""Submit payload to Deadline API end-point.
|
||||||
|
|
||||||
This takes payload in the form of JSON file and POST it to
|
This takes payload in the form of JSON file and POST it to
|
||||||
|
|
@ -592,6 +582,7 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin,
|
||||||
Args:
|
Args:
|
||||||
payload (dict): dict to become json in deadline submission.
|
payload (dict): dict to become json in deadline submission.
|
||||||
auth (tuple): (username, password)
|
auth (tuple): (username, password)
|
||||||
|
verify (bool): verify SSL certificate if present
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: resulting Deadline job id.
|
str: resulting Deadline job id.
|
||||||
|
|
@ -601,8 +592,8 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin,
|
||||||
|
|
||||||
"""
|
"""
|
||||||
url = "{}/api/jobs".format(self._deadline_url)
|
url = "{}/api/jobs".format(self._deadline_url)
|
||||||
response = requests_post(url, json=payload,
|
response = requests_post(
|
||||||
auth=auth)
|
url, json=payload, auth=auth, verify=verify)
|
||||||
if not response.ok:
|
if not response.ok:
|
||||||
self.log.error("Submission failed!")
|
self.log.error("Submission failed!")
|
||||||
self.log.error(response.status_code)
|
self.log.error(response.status_code)
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import six
|
||||||
from ayon_core.lib import Logger
|
from ayon_core.lib import Logger
|
||||||
from ayon_core.modules import AYONAddon, IPluginPaths
|
from ayon_core.modules import AYONAddon, IPluginPaths
|
||||||
|
|
||||||
|
from .version import __version__
|
||||||
|
|
||||||
|
|
||||||
class DeadlineWebserviceError(Exception):
|
class DeadlineWebserviceError(Exception):
|
||||||
"""
|
"""
|
||||||
|
|
@ -16,6 +18,7 @@ class DeadlineWebserviceError(Exception):
|
||||||
|
|
||||||
class DeadlineModule(AYONAddon, IPluginPaths):
|
class DeadlineModule(AYONAddon, IPluginPaths):
|
||||||
name = "deadline"
|
name = "deadline"
|
||||||
|
version = __version__
|
||||||
|
|
||||||
def initialize(self, studio_settings):
|
def initialize(self, studio_settings):
|
||||||
# This module is always enabled
|
# This module is always enabled
|
||||||
|
|
|
||||||
|
|
@ -26,27 +26,32 @@ class CollectDeadlinePools(pyblish.api.InstancePlugin,
|
||||||
|
|
||||||
order = pyblish.api.CollectorOrder + 0.420
|
order = pyblish.api.CollectorOrder + 0.420
|
||||||
label = "Collect Deadline Pools"
|
label = "Collect Deadline Pools"
|
||||||
hosts = ["aftereffects",
|
hosts = [
|
||||||
"fusion",
|
"aftereffects",
|
||||||
"harmony"
|
"fusion",
|
||||||
"nuke",
|
"harmony",
|
||||||
"maya",
|
"maya",
|
||||||
"max",
|
"max",
|
||||||
"houdini"]
|
"houdini",
|
||||||
|
"nuke",
|
||||||
|
]
|
||||||
|
|
||||||
families = ["render",
|
families = [
|
||||||
"rendering",
|
"render",
|
||||||
"render.farm",
|
"prerender",
|
||||||
"renderFarm",
|
"rendering",
|
||||||
"renderlayer",
|
"render.farm",
|
||||||
"maxrender",
|
"renderFarm",
|
||||||
"usdrender",
|
"renderlayer",
|
||||||
"redshift_rop",
|
"maxrender",
|
||||||
"arnold_rop",
|
"usdrender",
|
||||||
"mantra_rop",
|
"redshift_rop",
|
||||||
"karma_rop",
|
"arnold_rop",
|
||||||
"vray_rop",
|
"mantra_rop",
|
||||||
"publish.hou"]
|
"karma_rop",
|
||||||
|
"vray_rop",
|
||||||
|
"publish.hou",
|
||||||
|
]
|
||||||
|
|
||||||
primary_pool = None
|
primary_pool = None
|
||||||
secondary_pool = None
|
secondary_pool = None
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,9 @@ class CollectDeadlineUserCredentials(pyblish.api.InstancePlugin):
|
||||||
)
|
)
|
||||||
instance.data["deadline"]["auth"] = None
|
instance.data["deadline"]["auth"] = None
|
||||||
|
|
||||||
|
instance.data["deadline"]["verify"] = (
|
||||||
|
not deadline_info["not_verify_ssl"])
|
||||||
|
|
||||||
if not deadline_info["require_authentication"]:
|
if not deadline_info["require_authentication"]:
|
||||||
return
|
return
|
||||||
# TODO import 'get_addon_site_settings' when available
|
# TODO import 'get_addon_site_settings' when available
|
||||||
|
|
|
||||||
|
|
@ -174,8 +174,9 @@ class BlenderSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline,
|
||||||
instance.data["toBeRenderedOn"] = "deadline"
|
instance.data["toBeRenderedOn"] = "deadline"
|
||||||
|
|
||||||
payload = self.assemble_payload()
|
payload = self.assemble_payload()
|
||||||
return self.submit(payload,
|
auth = instance.data["deadline"]["auth"]
|
||||||
auth=instance.data["deadline"]["auth"])
|
verify = instance.data["deadline"]["verify"]
|
||||||
|
return self.submit(payload, auth=auth, verify=verify)
|
||||||
|
|
||||||
def from_published_scene(self):
|
def from_published_scene(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -193,9 +193,11 @@ class CelactionSubmitDeadline(pyblish.api.InstancePlugin):
|
||||||
self.expected_files(instance, render_path)
|
self.expected_files(instance, render_path)
|
||||||
self.log.debug("__ expectedFiles: `{}`".format(
|
self.log.debug("__ expectedFiles: `{}`".format(
|
||||||
instance.data["expectedFiles"]))
|
instance.data["expectedFiles"]))
|
||||||
|
auth = instance.data["deadline"]["auth"]
|
||||||
|
verify = instance.data["deadline"]["verify"]
|
||||||
response = requests_post(self.deadline_url, json=payload,
|
response = requests_post(self.deadline_url, json=payload,
|
||||||
auth=instance.data["deadline"]["require_authentication"])
|
auth=auth,
|
||||||
|
verify=verify)
|
||||||
|
|
||||||
if not response.ok:
|
if not response.ok:
|
||||||
self.log.error(
|
self.log.error(
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ class FusionSubmitDeadline(
|
||||||
else:
|
else:
|
||||||
context.data[key] = True
|
context.data[key] = True
|
||||||
|
|
||||||
from ayon_core.hosts.fusion.api.lib import get_frame_path
|
from ayon_fusion.api.lib import get_frame_path
|
||||||
|
|
||||||
deadline_url = instance.data["deadline"]["url"]
|
deadline_url = instance.data["deadline"]["url"]
|
||||||
assert deadline_url, "Requires Deadline Webservice URL"
|
assert deadline_url, "Requires Deadline Webservice URL"
|
||||||
|
|
@ -242,7 +242,8 @@ class FusionSubmitDeadline(
|
||||||
# E.g. http://192.168.0.1:8082/api/jobs
|
# E.g. http://192.168.0.1:8082/api/jobs
|
||||||
url = "{}/api/jobs".format(deadline_url)
|
url = "{}/api/jobs".format(deadline_url)
|
||||||
auth = instance.data["deadline"]["auth"]
|
auth = instance.data["deadline"]["auth"]
|
||||||
response = requests_post(url, json=payload, auth=auth)
|
verify = instance.data["deadline"]["verify"]
|
||||||
|
response = requests_post(url, json=payload, auth=auth, verify=verify)
|
||||||
if not response.ok:
|
if not response.ok:
|
||||||
raise Exception(response.text)
|
raise Exception(response.text)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ class HoudiniSubmitDeadline(
|
||||||
priority = 50
|
priority = 50
|
||||||
chunk_size = 1
|
chunk_size = 1
|
||||||
group = ""
|
group = ""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_attribute_defs(cls):
|
def get_attribute_defs(cls):
|
||||||
return [
|
return [
|
||||||
|
|
@ -188,7 +188,7 @@ class HoudiniSubmitDeadline(
|
||||||
|
|
||||||
job_info.Pool = instance.data.get("primaryPool")
|
job_info.Pool = instance.data.get("primaryPool")
|
||||||
job_info.SecondaryPool = instance.data.get("secondaryPool")
|
job_info.SecondaryPool = instance.data.get("secondaryPool")
|
||||||
|
|
||||||
if split_render_job and is_export_job:
|
if split_render_job and is_export_job:
|
||||||
job_info.Priority = attribute_values.get(
|
job_info.Priority = attribute_values.get(
|
||||||
"export_priority", self.export_priority
|
"export_priority", self.export_priority
|
||||||
|
|
@ -309,6 +309,11 @@ class HoudiniSubmitDeadline(
|
||||||
return attr.asdict(plugin_info)
|
return attr.asdict(plugin_info)
|
||||||
|
|
||||||
def process(self, instance):
|
def process(self, instance):
|
||||||
|
if not instance.data["farm"]:
|
||||||
|
self.log.debug("Render on farm is disabled. "
|
||||||
|
"Skipping deadline submission.")
|
||||||
|
return
|
||||||
|
|
||||||
super(HoudiniSubmitDeadline, self).process(instance)
|
super(HoudiniSubmitDeadline, self).process(instance)
|
||||||
|
|
||||||
# TODO: Avoid the need for this logic here, needed for submit publish
|
# TODO: Avoid the need for this logic here, needed for submit publish
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,11 @@ from ayon_core.pipeline.publish.lib import (
|
||||||
replace_with_published_scene_path
|
replace_with_published_scene_path
|
||||||
)
|
)
|
||||||
from ayon_core.pipeline.publish import KnownPublishError
|
from ayon_core.pipeline.publish import KnownPublishError
|
||||||
from ayon_core.hosts.max.api.lib import (
|
from ayon_max.api.lib import (
|
||||||
get_current_renderer,
|
get_current_renderer,
|
||||||
get_multipass_setting
|
get_multipass_setting
|
||||||
)
|
)
|
||||||
from ayon_core.hosts.max.api.lib_rendersettings import RenderSettings
|
from ayon_max.api.lib_rendersettings import RenderSettings
|
||||||
from openpype_modules.deadline import abstract_submit_deadline
|
from openpype_modules.deadline import abstract_submit_deadline
|
||||||
from openpype_modules.deadline.abstract_submit_deadline import DeadlineJobInfo
|
from openpype_modules.deadline.abstract_submit_deadline import DeadlineJobInfo
|
||||||
|
|
||||||
|
|
@ -181,27 +181,35 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline,
|
||||||
|
|
||||||
self.log.debug("Submitting 3dsMax render..")
|
self.log.debug("Submitting 3dsMax render..")
|
||||||
project_settings = instance.context.data["project_settings"]
|
project_settings = instance.context.data["project_settings"]
|
||||||
|
auth = instance.data["deadline"]["auth"]
|
||||||
|
verify = instance.data["deadline"]["verify"]
|
||||||
if instance.data.get("multiCamera"):
|
if instance.data.get("multiCamera"):
|
||||||
self.log.debug("Submitting jobs for multiple cameras..")
|
self.log.debug("Submitting jobs for multiple cameras..")
|
||||||
payload = self._use_published_name_for_multiples(
|
payload = self._use_published_name_for_multiples(
|
||||||
payload_data, project_settings)
|
payload_data, project_settings)
|
||||||
job_infos, plugin_infos = payload
|
job_infos, plugin_infos = payload
|
||||||
for job_info, plugin_info in zip(job_infos, plugin_infos):
|
for job_info, plugin_info in zip(job_infos, plugin_infos):
|
||||||
self.submit(self.assemble_payload(job_info, plugin_info),
|
self.submit(
|
||||||
instance.data["deadline"]["auth"])
|
self.assemble_payload(job_info, plugin_info),
|
||||||
|
auth=auth,
|
||||||
|
verify=verify
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
payload = self._use_published_name(payload_data, project_settings)
|
payload = self._use_published_name(payload_data, project_settings)
|
||||||
job_info, plugin_info = payload
|
job_info, plugin_info = payload
|
||||||
self.submit(self.assemble_payload(job_info, plugin_info),
|
self.submit(
|
||||||
instance.data["deadline"]["auth"])
|
self.assemble_payload(job_info, plugin_info),
|
||||||
|
auth=auth,
|
||||||
|
verify=verify
|
||||||
|
)
|
||||||
|
|
||||||
def _use_published_name(self, data, project_settings):
|
def _use_published_name(self, data, project_settings):
|
||||||
# Not all hosts can import these modules.
|
# Not all hosts can import these modules.
|
||||||
from ayon_core.hosts.max.api.lib import (
|
from ayon_max.api.lib import (
|
||||||
get_current_renderer,
|
get_current_renderer,
|
||||||
get_multipass_setting
|
get_multipass_setting
|
||||||
)
|
)
|
||||||
from ayon_core.hosts.max.api.lib_rendersettings import RenderSettings
|
from ayon_max.api.lib_rendersettings import RenderSettings
|
||||||
|
|
||||||
instance = self._instance
|
instance = self._instance
|
||||||
job_info = copy.deepcopy(self.job_info)
|
job_info = copy.deepcopy(self.job_info)
|
||||||
|
|
|
||||||
|
|
@ -39,8 +39,8 @@ from ayon_core.lib import (
|
||||||
EnumDef,
|
EnumDef,
|
||||||
is_in_tests,
|
is_in_tests,
|
||||||
)
|
)
|
||||||
from ayon_core.hosts.maya.api.lib_rendersettings import RenderSettings
|
from ayon_maya.api.lib_rendersettings import RenderSettings
|
||||||
from ayon_core.hosts.maya.api.lib import get_attr_in_layer
|
from ayon_maya.api.lib import get_attr_in_layer
|
||||||
|
|
||||||
from openpype_modules.deadline import abstract_submit_deadline
|
from openpype_modules.deadline import abstract_submit_deadline
|
||||||
from openpype_modules.deadline.abstract_submit_deadline import DeadlineJobInfo
|
from openpype_modules.deadline.abstract_submit_deadline import DeadlineJobInfo
|
||||||
|
|
@ -292,7 +292,7 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline,
|
||||||
|
|
||||||
return plugin_payload
|
return plugin_payload
|
||||||
|
|
||||||
def process_submission(self, auth=None):
|
def process_submission(self, auth=None, verify=True):
|
||||||
from maya import cmds
|
from maya import cmds
|
||||||
instance = self._instance
|
instance = self._instance
|
||||||
|
|
||||||
|
|
@ -332,8 +332,10 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline,
|
||||||
if "vrayscene" in instance.data["families"]:
|
if "vrayscene" in instance.data["families"]:
|
||||||
self.log.debug("Submitting V-Ray scene render..")
|
self.log.debug("Submitting V-Ray scene render..")
|
||||||
vray_export_payload = self._get_vray_export_payload(payload_data)
|
vray_export_payload = self._get_vray_export_payload(payload_data)
|
||||||
|
|
||||||
export_job = self.submit(vray_export_payload,
|
export_job = self.submit(vray_export_payload,
|
||||||
instance.data["deadline"]["auth"])
|
auth=auth,
|
||||||
|
verify=verify)
|
||||||
|
|
||||||
payload = self._get_vray_render_payload(payload_data)
|
payload = self._get_vray_render_payload(payload_data)
|
||||||
|
|
||||||
|
|
@ -353,7 +355,8 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline,
|
||||||
# Submit main render job
|
# Submit main render job
|
||||||
job_info, plugin_info = payload
|
job_info, plugin_info = payload
|
||||||
self.submit(self.assemble_payload(job_info, plugin_info),
|
self.submit(self.assemble_payload(job_info, plugin_info),
|
||||||
instance.data["deadline"]["auth"])
|
auth=auth,
|
||||||
|
verify=verify)
|
||||||
|
|
||||||
def _tile_render(self, payload):
|
def _tile_render(self, payload):
|
||||||
"""Submit as tile render per frame with dependent assembly jobs."""
|
"""Submit as tile render per frame with dependent assembly jobs."""
|
||||||
|
|
@ -557,13 +560,18 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline,
|
||||||
# Submit assembly jobs
|
# Submit assembly jobs
|
||||||
assembly_job_ids = []
|
assembly_job_ids = []
|
||||||
num_assemblies = len(assembly_payloads)
|
num_assemblies = len(assembly_payloads)
|
||||||
|
auth = instance.data["deadline"]["auth"]
|
||||||
|
verify = instance.data["deadline"]["verify"]
|
||||||
for i, payload in enumerate(assembly_payloads):
|
for i, payload in enumerate(assembly_payloads):
|
||||||
self.log.debug(
|
self.log.debug(
|
||||||
"submitting assembly job {} of {}".format(i + 1,
|
"submitting assembly job {} of {}".format(i + 1,
|
||||||
num_assemblies)
|
num_assemblies)
|
||||||
)
|
)
|
||||||
assembly_job_id = self.submit(payload,
|
assembly_job_id = self.submit(
|
||||||
instance.data["deadline"]["auth"])
|
payload,
|
||||||
|
auth=auth,
|
||||||
|
verify=verify
|
||||||
|
)
|
||||||
assembly_job_ids.append(assembly_job_id)
|
assembly_job_ids.append(assembly_job_id)
|
||||||
|
|
||||||
instance.data["assemblySubmissionJobs"] = assembly_job_ids
|
instance.data["assemblySubmissionJobs"] = assembly_job_ids
|
||||||
|
|
|
||||||
|
|
@ -424,8 +424,12 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin,
|
||||||
self.log.debug("__ expectedFiles: `{}`".format(
|
self.log.debug("__ expectedFiles: `{}`".format(
|
||||||
instance.data["expectedFiles"]))
|
instance.data["expectedFiles"]))
|
||||||
auth = instance.data["deadline"]["auth"]
|
auth = instance.data["deadline"]["auth"]
|
||||||
response = requests_post(self.deadline_url, json=payload, timeout=10,
|
verify = instance.data["deadline"]["verify"]
|
||||||
auth=auth)
|
response = requests_post(self.deadline_url,
|
||||||
|
json=payload,
|
||||||
|
timeout=10,
|
||||||
|
auth=auth,
|
||||||
|
verify=verify)
|
||||||
|
|
||||||
if not response.ok:
|
if not response.ok:
|
||||||
raise Exception(response.text)
|
raise Exception(response.text)
|
||||||
|
|
|
||||||
|
|
@ -210,8 +210,9 @@ class ProcessSubmittedCacheJobOnFarm(pyblish.api.InstancePlugin,
|
||||||
|
|
||||||
url = "{}/api/jobs".format(self.deadline_url)
|
url = "{}/api/jobs".format(self.deadline_url)
|
||||||
auth = instance.data["deadline"]["auth"]
|
auth = instance.data["deadline"]["auth"]
|
||||||
response = requests_post(url, json=payload, timeout=10,
|
verify = instance.data["deadline"]["verify"]
|
||||||
auth=auth)
|
response = requests_post(
|
||||||
|
url, json=payload, timeout=10, auth=auth, verify=verify)
|
||||||
if not response.ok:
|
if not response.ok:
|
||||||
raise Exception(response.text)
|
raise Exception(response.text)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -304,8 +304,9 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin,
|
||||||
|
|
||||||
url = "{}/api/jobs".format(self.deadline_url)
|
url = "{}/api/jobs".format(self.deadline_url)
|
||||||
auth = instance.data["deadline"]["auth"]
|
auth = instance.data["deadline"]["auth"]
|
||||||
response = requests_post(url, json=payload, timeout=10,
|
verify = instance.data["deadline"]["verify"]
|
||||||
auth=auth)
|
response = requests_post(
|
||||||
|
url, json=payload, timeout=10, auth=auth, verify=verify)
|
||||||
if not response.ok:
|
if not response.ok:
|
||||||
raise Exception(response.text)
|
raise Exception(response.text)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
__version__ = "0.1.10"
|
__version__ = "0.1.12"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
|
from .version import __version__
|
||||||
from .addon import JobQueueAddon
|
from .addon import JobQueueAddon
|
||||||
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
"__version__",
|
||||||
|
|
||||||
"JobQueueAddon",
|
"JobQueueAddon",
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -44,9 +44,12 @@ import platform
|
||||||
from ayon_core.addon import AYONAddon, click_wrap
|
from ayon_core.addon import AYONAddon, click_wrap
|
||||||
from ayon_core.settings import get_studio_settings
|
from ayon_core.settings import get_studio_settings
|
||||||
|
|
||||||
|
from .version import __version__
|
||||||
|
|
||||||
|
|
||||||
class JobQueueAddon(AYONAddon):
|
class JobQueueAddon(AYONAddon):
|
||||||
name = "job_queue"
|
name = "job_queue"
|
||||||
|
version = __version__
|
||||||
|
|
||||||
def initialize(self, studio_settings):
|
def initialize(self, studio_settings):
|
||||||
addon_settings = studio_settings.get(self.name) or {}
|
addon_settings = studio_settings.get(self.name) or {}
|
||||||
|
|
|
||||||
1
client/ayon_core/modules/job_queue/version.py
Normal file
1
client/ayon_core/modules/job_queue/version.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
__version__ = "1.0.0"
|
||||||
|
|
@ -7,6 +7,7 @@ from ayon_core.addon import AYONAddon, ITrayAction
|
||||||
class LauncherAction(AYONAddon, ITrayAction):
|
class LauncherAction(AYONAddon, ITrayAction):
|
||||||
label = "Launcher"
|
label = "Launcher"
|
||||||
name = "launcher_tool"
|
name = "launcher_tool"
|
||||||
|
version = "1.0.0"
|
||||||
|
|
||||||
def initialize(self, settings):
|
def initialize(self, settings):
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ from ayon_core.addon import AYONAddon, ITrayAddon
|
||||||
|
|
||||||
class LoaderAddon(AYONAddon, ITrayAddon):
|
class LoaderAddon(AYONAddon, ITrayAddon):
|
||||||
name = "loader_tool"
|
name = "loader_tool"
|
||||||
|
version = "1.0.0"
|
||||||
|
|
||||||
def initialize(self, settings):
|
def initialize(self, settings):
|
||||||
# Tray attributes
|
# Tray attributes
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ from ayon_core.addon import AYONAddon, ITrayAction
|
||||||
class PythonInterpreterAction(AYONAddon, ITrayAction):
|
class PythonInterpreterAction(AYONAddon, ITrayAction):
|
||||||
label = "Console"
|
label = "Console"
|
||||||
name = "python_interpreter"
|
name = "python_interpreter"
|
||||||
|
version = "1.0.0"
|
||||||
admin_action = True
|
admin_action = True
|
||||||
|
|
||||||
def initialize(self, settings):
|
def initialize(self, settings):
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
|
from .version import __version__
|
||||||
from .addon import RoyalRenderAddon
|
from .addon import RoyalRenderAddon
|
||||||
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
"__version__",
|
||||||
|
|
||||||
"RoyalRenderAddon",
|
"RoyalRenderAddon",
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,13 @@ import os
|
||||||
|
|
||||||
from ayon_core.addon import AYONAddon, IPluginPaths
|
from ayon_core.addon import AYONAddon, IPluginPaths
|
||||||
|
|
||||||
|
from .version import __version__
|
||||||
|
|
||||||
|
|
||||||
class RoyalRenderAddon(AYONAddon, IPluginPaths):
|
class RoyalRenderAddon(AYONAddon, IPluginPaths):
|
||||||
"""Class providing basic Royal Render implementation logic."""
|
"""Class providing basic Royal Render implementation logic."""
|
||||||
name = "royalrender"
|
name = "royalrender"
|
||||||
|
version = __version__
|
||||||
|
|
||||||
# _rr_api = None
|
# _rr_api = None
|
||||||
# @property
|
# @property
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ from ayon_core.lib import Logger, run_subprocess, AYONSettingsRegistry
|
||||||
from ayon_core.lib.vendor_bin_utils import find_tool_in_custom_paths
|
from ayon_core.lib.vendor_bin_utils import find_tool_in_custom_paths
|
||||||
|
|
||||||
from .rr_job import SubmitFile
|
from .rr_job import SubmitFile
|
||||||
from .rr_job import RRjob, SubmitterParameter # noqa F401
|
from .rr_job import RRJob, SubmitterParameter # noqa F401
|
||||||
|
|
||||||
|
|
||||||
class Api:
|
class Api:
|
||||||
|
|
|
||||||
1
client/ayon_core/modules/royalrender/version.py
Normal file
1
client/ayon_core/modules/royalrender/version.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
__version__ = "0.1.1"
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
|
from .version import __version__
|
||||||
from .timers_manager import (
|
from .timers_manager import (
|
||||||
TimersManager
|
TimersManager
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
"__version__",
|
||||||
|
|
||||||
"TimersManager",
|
"TimersManager",
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ from ayon_core.addon import (
|
||||||
)
|
)
|
||||||
from ayon_core.lib.events import register_event_callback
|
from ayon_core.lib.events import register_event_callback
|
||||||
|
|
||||||
|
from .version import __version__
|
||||||
from .exceptions import InvalidContextError
|
from .exceptions import InvalidContextError
|
||||||
|
|
||||||
TIMER_MODULE_DIR = os.path.dirname(os.path.abspath(__file__))
|
TIMER_MODULE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
@ -96,6 +97,7 @@ class TimersManager(
|
||||||
See `ExampleTimersManagerConnector`.
|
See `ExampleTimersManagerConnector`.
|
||||||
"""
|
"""
|
||||||
name = "timers_manager"
|
name = "timers_manager"
|
||||||
|
version = __version__
|
||||||
label = "Timers Service"
|
label = "Timers Service"
|
||||||
|
|
||||||
_required_methods = (
|
_required_methods = (
|
||||||
|
|
|
||||||
1
client/ayon_core/modules/timers_manager/version.py
Normal file
1
client/ayon_core/modules/timers_manager/version.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
__version__ = "0.1.1"
|
||||||
|
|
@ -1,8 +1,13 @@
|
||||||
|
from .version import __version__
|
||||||
|
from .structures import HostMsgAction
|
||||||
from .webserver_module import (
|
from .webserver_module import (
|
||||||
WebServerAddon
|
WebServerAddon
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
"__version__",
|
||||||
|
|
||||||
|
"HostMsgAction",
|
||||||
"WebServerAddon",
|
"WebServerAddon",
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -9,22 +9,18 @@ from qtpy import QtWidgets
|
||||||
from ayon_core.addon import ITrayService
|
from ayon_core.addon import ITrayService
|
||||||
from ayon_core.tools.stdout_broker.window import ConsoleDialog
|
from ayon_core.tools.stdout_broker.window import ConsoleDialog
|
||||||
|
|
||||||
|
from .structures import HostMsgAction
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# Host listener icon type
|
||||||
class IconType:
|
class IconType:
|
||||||
IDLE = "idle"
|
IDLE = "idle"
|
||||||
RUNNING = "running"
|
RUNNING = "running"
|
||||||
FAILED = "failed"
|
FAILED = "failed"
|
||||||
|
|
||||||
|
|
||||||
class MsgAction:
|
|
||||||
CONNECTING = "connecting"
|
|
||||||
INITIALIZED = "initialized"
|
|
||||||
ADD = "add"
|
|
||||||
CLOSE = "close"
|
|
||||||
|
|
||||||
|
|
||||||
class HostListener:
|
class HostListener:
|
||||||
def __init__(self, webserver, module):
|
def __init__(self, webserver, module):
|
||||||
self._window_per_id = {}
|
self._window_per_id = {}
|
||||||
|
|
@ -96,22 +92,22 @@ class HostListener:
|
||||||
if msg.type == aiohttp.WSMsgType.TEXT:
|
if msg.type == aiohttp.WSMsgType.TEXT:
|
||||||
host_name, action, text = self._parse_message(msg)
|
host_name, action, text = self._parse_message(msg)
|
||||||
|
|
||||||
if action == MsgAction.CONNECTING:
|
if action == HostMsgAction.CONNECTING:
|
||||||
self._action_per_id[host_name] = None
|
self._action_per_id[host_name] = None
|
||||||
# must be sent to main thread, or action wont trigger
|
# must be sent to main thread, or action wont trigger
|
||||||
self.module.execute_in_main_thread(
|
self.module.execute_in_main_thread(
|
||||||
lambda: self._host_is_connecting(host_name, text))
|
lambda: self._host_is_connecting(host_name, text))
|
||||||
elif action == MsgAction.CLOSE:
|
elif action == HostMsgAction.CLOSE:
|
||||||
# clean close
|
# clean close
|
||||||
self._close(host_name)
|
self._close(host_name)
|
||||||
await ws.close()
|
await ws.close()
|
||||||
elif action == MsgAction.INITIALIZED:
|
elif action == HostMsgAction.INITIALIZED:
|
||||||
self.module.execute_in_main_thread(
|
self.module.execute_in_main_thread(
|
||||||
# must be queued as _host_is_connecting might not
|
# must be queued as _host_is_connecting might not
|
||||||
# be triggered/finished yet
|
# be triggered/finished yet
|
||||||
lambda: self._set_host_icon(host_name,
|
lambda: self._set_host_icon(host_name,
|
||||||
IconType.RUNNING))
|
IconType.RUNNING))
|
||||||
elif action == MsgAction.ADD:
|
elif action == HostMsgAction.ADD:
|
||||||
self.module.execute_in_main_thread(
|
self.module.execute_in_main_thread(
|
||||||
lambda: self._add_text(host_name, text))
|
lambda: self._add_text(host_name, text))
|
||||||
elif msg.type == aiohttp.WSMsgType.ERROR:
|
elif msg.type == aiohttp.WSMsgType.ERROR:
|
||||||
|
|
|
||||||
6
client/ayon_core/modules/webserver/structures.py
Normal file
6
client/ayon_core/modules/webserver/structures.py
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
# Host listener message actions
|
||||||
|
class HostMsgAction:
|
||||||
|
CONNECTING = "connecting"
|
||||||
|
INITIALIZED = "initialized"
|
||||||
|
ADD = "add"
|
||||||
|
CLOSE = "close"
|
||||||
1
client/ayon_core/modules/webserver/version.py
Normal file
1
client/ayon_core/modules/webserver/version.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
__version__ = "1.0.0"
|
||||||
|
|
@ -26,9 +26,12 @@ import socket
|
||||||
from ayon_core import resources
|
from ayon_core import resources
|
||||||
from ayon_core.addon import AYONAddon, ITrayService
|
from ayon_core.addon import AYONAddon, ITrayService
|
||||||
|
|
||||||
|
from .version import __version__
|
||||||
|
|
||||||
|
|
||||||
class WebServerAddon(AYONAddon, ITrayService):
|
class WebServerAddon(AYONAddon, ITrayService):
|
||||||
name = "webserver"
|
name = "webserver"
|
||||||
|
version = __version__
|
||||||
label = "WebServer"
|
label = "WebServer"
|
||||||
|
|
||||||
webserver_url_env = "AYON_WEBSERVER_URL"
|
webserver_url_env = "AYON_WEBSERVER_URL"
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -459,36 +459,6 @@ def is_representation_from_latest(representation):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_template_data_from_session(session=None, settings=None):
|
|
||||||
"""Template data for template fill from session keys.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
session (Union[Dict[str, str], None]): The Session to use. If not
|
|
||||||
provided use the currently active global Session.
|
|
||||||
settings (Optional[Dict[str, Any]]): Prepared studio or project
|
|
||||||
settings.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict[str, Any]: All available data from session.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if session is not None:
|
|
||||||
project_name = session["AYON_PROJECT_NAME"]
|
|
||||||
folder_path = session["AYON_FOLDER_PATH"]
|
|
||||||
task_name = session["AYON_TASK_NAME"]
|
|
||||||
host_name = session["AYON_HOST_NAME"]
|
|
||||||
else:
|
|
||||||
context = get_current_context()
|
|
||||||
project_name = context["project_name"]
|
|
||||||
folder_path = context["folder_path"]
|
|
||||||
task_name = context["task_name"]
|
|
||||||
host_name = get_current_host_name()
|
|
||||||
|
|
||||||
return get_template_data_with_names(
|
|
||||||
project_name, folder_path, task_name, host_name, settings
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_current_context_template_data(settings=None):
|
def get_current_context_template_data(settings=None):
|
||||||
"""Prepare template data for current context.
|
"""Prepare template data for current context.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -681,7 +681,7 @@ class PublishAttributeValues(AttributeValues):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def parent(self):
|
def parent(self):
|
||||||
self.publish_attributes.parent
|
return self.publish_attributes.parent
|
||||||
|
|
||||||
|
|
||||||
class PublishAttributes:
|
class PublishAttributes:
|
||||||
|
|
|
||||||
|
|
@ -336,17 +336,16 @@ def get_plugin_settings(plugin, project_settings, log, category=None):
|
||||||
settings_category = getattr(plugin, "settings_category", None)
|
settings_category = getattr(plugin, "settings_category", None)
|
||||||
if settings_category:
|
if settings_category:
|
||||||
try:
|
try:
|
||||||
return (
|
category_settings = project_settings[settings_category]
|
||||||
project_settings
|
|
||||||
[settings_category]
|
|
||||||
["publish"]
|
|
||||||
[plugin.__name__]
|
|
||||||
)
|
|
||||||
except KeyError:
|
except KeyError:
|
||||||
log.warning((
|
log.warning((
|
||||||
"Couldn't find plugin '{}' settings"
|
"Couldn't find settings category '{}' in project settings"
|
||||||
" under settings category '{}'"
|
).format(settings_category))
|
||||||
).format(plugin.__name__, settings_category))
|
return {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
return category_settings["publish"][plugin.__name__]
|
||||||
|
except KeyError:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
# Use project settings based on a category name
|
# Use project settings based on a category name
|
||||||
|
|
|
||||||
|
|
@ -108,69 +108,6 @@ class IntegrateAsset(pyblish.api.InstancePlugin):
|
||||||
|
|
||||||
label = "Integrate Asset"
|
label = "Integrate Asset"
|
||||||
order = pyblish.api.IntegratorOrder
|
order = pyblish.api.IntegratorOrder
|
||||||
families = ["workfile",
|
|
||||||
"pointcache",
|
|
||||||
"pointcloud",
|
|
||||||
"proxyAbc",
|
|
||||||
"camera",
|
|
||||||
"animation",
|
|
||||||
"model",
|
|
||||||
"maxScene",
|
|
||||||
"mayaAscii",
|
|
||||||
"mayaScene",
|
|
||||||
"setdress",
|
|
||||||
"layout",
|
|
||||||
"ass",
|
|
||||||
"assProxy",
|
|
||||||
"vdbcache",
|
|
||||||
"scene",
|
|
||||||
"vrayproxy",
|
|
||||||
"vrayscene_layer",
|
|
||||||
"render",
|
|
||||||
"prerender",
|
|
||||||
"imagesequence",
|
|
||||||
"review",
|
|
||||||
"rendersetup",
|
|
||||||
"rig",
|
|
||||||
"plate",
|
|
||||||
"look",
|
|
||||||
"ociolook",
|
|
||||||
"audio",
|
|
||||||
"yetiRig",
|
|
||||||
"yeticache",
|
|
||||||
"nukenodes",
|
|
||||||
"gizmo",
|
|
||||||
"source",
|
|
||||||
"matchmove",
|
|
||||||
"image",
|
|
||||||
"assembly",
|
|
||||||
"fbx",
|
|
||||||
"gltf",
|
|
||||||
"textures",
|
|
||||||
"action",
|
|
||||||
"harmony.template",
|
|
||||||
"harmony.palette",
|
|
||||||
"editorial",
|
|
||||||
"background",
|
|
||||||
"camerarig",
|
|
||||||
"redshiftproxy",
|
|
||||||
"effect",
|
|
||||||
"xgen",
|
|
||||||
"hda",
|
|
||||||
"usd",
|
|
||||||
"staticMesh",
|
|
||||||
"skeletalMesh",
|
|
||||||
"mvLook",
|
|
||||||
"mvUsd",
|
|
||||||
"mvUsdComposition",
|
|
||||||
"mvUsdOverride",
|
|
||||||
"online",
|
|
||||||
"uasset",
|
|
||||||
"blendScene",
|
|
||||||
"yeticacheUE",
|
|
||||||
"tycache",
|
|
||||||
"csv_ingest_file",
|
|
||||||
]
|
|
||||||
|
|
||||||
default_template_name = "publish"
|
default_template_name = "publish"
|
||||||
|
|
||||||
|
|
@ -360,7 +297,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin):
|
||||||
|
|
||||||
# Compute the resource file infos once (files belonging to the
|
# Compute the resource file infos once (files belonging to the
|
||||||
# version instance instead of an individual representation) so
|
# version instance instead of an individual representation) so
|
||||||
# we can re-use those file infos per representation
|
# we can reuse those file infos per representation
|
||||||
resource_file_infos = self.get_files_info(
|
resource_file_infos = self.get_files_info(
|
||||||
resource_destinations, anatomy
|
resource_destinations, anatomy
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
import pyblish.api
|
import pyblish.api
|
||||||
|
|
||||||
|
from ayon_core.lib import filter_profiles
|
||||||
|
from ayon_core.host import ILoadHost
|
||||||
from ayon_core.pipeline.load import any_outdated_containers
|
from ayon_core.pipeline.load import any_outdated_containers
|
||||||
from ayon_core.pipeline import (
|
from ayon_core.pipeline import (
|
||||||
|
get_current_host_name,
|
||||||
|
registered_host,
|
||||||
PublishXmlValidationError,
|
PublishXmlValidationError,
|
||||||
OptionalPyblishPluginMixin
|
OptionalPyblishPluginMixin
|
||||||
)
|
)
|
||||||
|
|
@ -18,17 +23,50 @@ class ShowInventory(pyblish.api.Action):
|
||||||
host_tools.show_scene_inventory()
|
host_tools.show_scene_inventory()
|
||||||
|
|
||||||
|
|
||||||
class ValidateContainers(OptionalPyblishPluginMixin,
|
class ValidateOutdatedContainers(
|
||||||
pyblish.api.ContextPlugin):
|
OptionalPyblishPluginMixin,
|
||||||
|
pyblish.api.ContextPlugin
|
||||||
|
):
|
||||||
"""Containers are must be updated to latest version on publish."""
|
"""Containers are must be updated to latest version on publish."""
|
||||||
|
|
||||||
label = "Validate Outdated Containers"
|
label = "Validate Outdated Containers"
|
||||||
order = pyblish.api.ValidatorOrder
|
order = pyblish.api.ValidatorOrder
|
||||||
hosts = ["maya", "houdini", "nuke", "harmony", "photoshop", "aftereffects"]
|
|
||||||
optional = True
|
optional = True
|
||||||
actions = [ShowInventory]
|
actions = [ShowInventory]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def apply_settings(cls, settings):
|
||||||
|
# Disable plugin if host does not inherit from 'ILoadHost'
|
||||||
|
# - not a host that can load containers
|
||||||
|
host = registered_host()
|
||||||
|
if not isinstance(host, ILoadHost):
|
||||||
|
cls.enabled = False
|
||||||
|
return
|
||||||
|
|
||||||
|
# Disable if no profile is found for the current host
|
||||||
|
profiles = (
|
||||||
|
settings
|
||||||
|
["core"]
|
||||||
|
["publish"]
|
||||||
|
["ValidateOutdatedContainers"]
|
||||||
|
["plugin_state_profiles"]
|
||||||
|
)
|
||||||
|
profile = filter_profiles(
|
||||||
|
profiles, {"host_names": get_current_host_name()}
|
||||||
|
)
|
||||||
|
if not profile:
|
||||||
|
cls.enabled = False
|
||||||
|
return
|
||||||
|
|
||||||
|
# Apply settings from profile
|
||||||
|
for attr_name in {
|
||||||
|
"enabled",
|
||||||
|
"optional",
|
||||||
|
"active",
|
||||||
|
}:
|
||||||
|
setattr(cls, attr_name, profile[attr_name])
|
||||||
|
|
||||||
def process(self, context):
|
def process(self, context):
|
||||||
if not self.is_active(context.data):
|
if not self.is_active(context.data):
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,14 @@
|
||||||
import pyblish.api
|
import pyblish.api
|
||||||
from ayon_core.pipeline.publish import PublishValidationError
|
|
||||||
|
from ayon_core.lib import filter_profiles
|
||||||
|
from ayon_core.pipeline.publish import (
|
||||||
|
PublishValidationError,
|
||||||
|
OptionalPyblishPluginMixin
|
||||||
|
)
|
||||||
|
from ayon_core.pipeline import get_current_host_name
|
||||||
|
|
||||||
|
|
||||||
class ValidateVersion(pyblish.api.InstancePlugin):
|
class ValidateVersion(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin):
|
||||||
"""Validate instance version.
|
"""Validate instance version.
|
||||||
|
|
||||||
AYON does not allow overwriting previously published versions.
|
AYON does not allow overwriting previously published versions.
|
||||||
|
|
@ -11,13 +17,39 @@ class ValidateVersion(pyblish.api.InstancePlugin):
|
||||||
order = pyblish.api.ValidatorOrder
|
order = pyblish.api.ValidatorOrder
|
||||||
|
|
||||||
label = "Validate Version"
|
label = "Validate Version"
|
||||||
hosts = ["nuke", "maya", "houdini", "blender",
|
|
||||||
"photoshop", "aftereffects"]
|
|
||||||
|
|
||||||
optional = False
|
optional = False
|
||||||
active = True
|
active = True
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def apply_settings(cls, settings):
|
||||||
|
# Disable if no profile is found for the current host
|
||||||
|
profiles = (
|
||||||
|
settings
|
||||||
|
["core"]
|
||||||
|
["publish"]
|
||||||
|
["ValidateVersion"]
|
||||||
|
["plugin_state_profiles"]
|
||||||
|
)
|
||||||
|
profile = filter_profiles(
|
||||||
|
profiles, {"host_names": get_current_host_name()}
|
||||||
|
)
|
||||||
|
if not profile:
|
||||||
|
cls.enabled = False
|
||||||
|
return
|
||||||
|
|
||||||
|
# Apply settings from profile
|
||||||
|
for attr_name in {
|
||||||
|
"enabled",
|
||||||
|
"optional",
|
||||||
|
"active",
|
||||||
|
}:
|
||||||
|
setattr(cls, attr_name, profile[attr_name])
|
||||||
|
|
||||||
def process(self, instance):
|
def process(self, instance):
|
||||||
|
if not self.is_active(instance.data):
|
||||||
|
return
|
||||||
|
|
||||||
version = instance.data.get("version")
|
version = instance.data.get("version")
|
||||||
latest_version = instance.data.get("latestVersion")
|
latest_version = instance.data.get("latestVersion")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,31 @@
|
||||||
"""OpenColorIO Wrapper.
|
"""OpenColorIO Wrapper.
|
||||||
|
|
||||||
Only to be interpreted by Python 3. It is run in subprocess in case
|
Receive OpenColorIO information and store it in JSON format for processed
|
||||||
Python 2 hosts needs to use it. Or it is used as module for Python 3
|
that don't have access to OpenColorIO or their version of OpenColorIO is
|
||||||
processing.
|
not compatible.
|
||||||
|
|
||||||
Providing functionality:
|
|
||||||
- get_colorspace - console command - python 2
|
|
||||||
- returning all available color spaces
|
|
||||||
found in input config path.
|
|
||||||
- _get_colorspace_data - python 3 - module function
|
|
||||||
- returning all available colorspaces
|
|
||||||
found in input config path.
|
|
||||||
- get_views - console command - python 2
|
|
||||||
- returning all available viewers
|
|
||||||
found in input config path.
|
|
||||||
- _get_views_data - python 3 - module function
|
|
||||||
- returning all available viewers
|
|
||||||
found in input config path.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import click
|
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import PyOpenColorIO as ocio
|
|
||||||
|
import click
|
||||||
|
|
||||||
|
from ayon_core.pipeline.colorspace import (
|
||||||
|
has_compatible_ocio_package,
|
||||||
|
get_display_view_colorspace_name,
|
||||||
|
get_config_file_rules_colorspace_from_filepath,
|
||||||
|
get_config_version_data,
|
||||||
|
get_ocio_config_views,
|
||||||
|
get_ocio_config_colorspaces,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _save_output_to_json_file(output, output_path):
|
||||||
|
json_path = Path(output_path)
|
||||||
|
with open(json_path, "w") as stream:
|
||||||
|
json.dump(output, stream)
|
||||||
|
|
||||||
|
print(f"Data are saved to '{json_path}'")
|
||||||
|
|
||||||
|
|
||||||
@click.group()
|
@click.group()
|
||||||
|
|
@ -30,404 +33,185 @@ def main():
|
||||||
pass # noqa: WPS100
|
pass # noqa: WPS100
|
||||||
|
|
||||||
|
|
||||||
@main.group()
|
@main.command(
|
||||||
def config():
|
name="get_ocio_config_colorspaces",
|
||||||
"""Config related commands group
|
help="return all colorspaces from config file")
|
||||||
|
@click.option(
|
||||||
Example of use:
|
"--config_path",
|
||||||
> pyton.exe ./ocio_wrapper.py config <command> *args
|
required=True,
|
||||||
"""
|
help="OCIO config path to read ocio config file.",
|
||||||
pass # noqa: WPS100
|
type=click.Path(exists=True))
|
||||||
|
@click.option(
|
||||||
|
"--output_path",
|
||||||
@main.group()
|
required=True,
|
||||||
def colorspace():
|
help="path where to write output json file",
|
||||||
"""Colorspace related commands group
|
type=click.Path())
|
||||||
|
def _get_ocio_config_colorspaces(config_path, output_path):
|
||||||
Example of use:
|
|
||||||
> pyton.exe ./ocio_wrapper.py config <command> *args
|
|
||||||
"""
|
|
||||||
pass # noqa: WPS100
|
|
||||||
|
|
||||||
|
|
||||||
@config.command(
|
|
||||||
name="get_colorspace",
|
|
||||||
help=(
|
|
||||||
"return all colorspaces from config file "
|
|
||||||
"--path input arg is required"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
@click.option("--in_path", required=True,
|
|
||||||
help="path where to read ocio config file",
|
|
||||||
type=click.Path(exists=True))
|
|
||||||
@click.option("--out_path", required=True,
|
|
||||||
help="path where to write output json file",
|
|
||||||
type=click.Path())
|
|
||||||
def get_colorspace(in_path, out_path):
|
|
||||||
"""Aggregate all colorspace to file.
|
"""Aggregate all colorspace to file.
|
||||||
|
|
||||||
Python 2 wrapped console command
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
in_path (str): config file path string
|
config_path (str): config file path string
|
||||||
out_path (str): temp json file path string
|
output_path (str): temp json file path string
|
||||||
|
|
||||||
Example of use:
|
Example of use:
|
||||||
> pyton.exe ./ocio_wrapper.py config get_colorspace
|
> pyton.exe ./ocio_wrapper.py config get_colorspace
|
||||||
--in_path=<path> --out_path=<path>
|
--config_path <path> --output_path <path>
|
||||||
"""
|
"""
|
||||||
json_path = Path(out_path)
|
_save_output_to_json_file(
|
||||||
|
get_ocio_config_colorspaces(config_path),
|
||||||
out_data = _get_colorspace_data(in_path)
|
output_path
|
||||||
|
|
||||||
with open(json_path, "w") as f_:
|
|
||||||
json.dump(out_data, f_)
|
|
||||||
|
|
||||||
print(f"Colorspace data are saved to '{json_path}'")
|
|
||||||
|
|
||||||
|
|
||||||
def _get_colorspace_data(config_path):
|
|
||||||
"""Return all found colorspace data.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
config_path (str): path string leading to config.ocio
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
IOError: Input config does not exist.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: aggregated available colorspaces
|
|
||||||
"""
|
|
||||||
config_path = Path(config_path)
|
|
||||||
|
|
||||||
if not config_path.is_file():
|
|
||||||
raise IOError(
|
|
||||||
f"Input path `{config_path}` should be `config.ocio` file")
|
|
||||||
|
|
||||||
config = ocio.Config().CreateFromFile(str(config_path))
|
|
||||||
|
|
||||||
colorspace_data = {
|
|
||||||
"roles": {},
|
|
||||||
"colorspaces": {
|
|
||||||
color.getName(): {
|
|
||||||
"family": color.getFamily(),
|
|
||||||
"categories": list(color.getCategories()),
|
|
||||||
"aliases": list(color.getAliases()),
|
|
||||||
"equalitygroup": color.getEqualityGroup(),
|
|
||||||
}
|
|
||||||
for color in config.getColorSpaces()
|
|
||||||
},
|
|
||||||
"displays_views": {
|
|
||||||
f"{view} ({display})": {
|
|
||||||
"display": display,
|
|
||||||
"view": view
|
|
||||||
|
|
||||||
}
|
|
||||||
for display in config.getDisplays()
|
|
||||||
for view in config.getViews(display)
|
|
||||||
},
|
|
||||||
"looks": {}
|
|
||||||
}
|
|
||||||
|
|
||||||
# add looks
|
|
||||||
looks = config.getLooks()
|
|
||||||
if looks:
|
|
||||||
colorspace_data["looks"] = {
|
|
||||||
look.getName(): {"process_space": look.getProcessSpace()}
|
|
||||||
for look in looks
|
|
||||||
}
|
|
||||||
|
|
||||||
# add roles
|
|
||||||
roles = config.getRoles()
|
|
||||||
if roles:
|
|
||||||
colorspace_data["roles"] = {
|
|
||||||
role: {"colorspace": colorspace}
|
|
||||||
for (role, colorspace) in roles
|
|
||||||
}
|
|
||||||
|
|
||||||
return colorspace_data
|
|
||||||
|
|
||||||
|
|
||||||
@config.command(
|
|
||||||
name="get_views",
|
|
||||||
help=(
|
|
||||||
"return all viewers from config file "
|
|
||||||
"--path input arg is required"
|
|
||||||
)
|
)
|
||||||
)
|
|
||||||
@click.option("--in_path", required=True,
|
|
||||||
help="path where to read ocio config file",
|
@main.command(
|
||||||
type=click.Path(exists=True))
|
name="get_ocio_config_views",
|
||||||
@click.option("--out_path", required=True,
|
help="All viewers from config file")
|
||||||
help="path where to write output json file",
|
@click.option(
|
||||||
type=click.Path())
|
"--config_path",
|
||||||
def get_views(in_path, out_path):
|
required=True,
|
||||||
|
help="OCIO config path to read ocio config file.",
|
||||||
|
type=click.Path(exists=True))
|
||||||
|
@click.option(
|
||||||
|
"--output_path",
|
||||||
|
required=True,
|
||||||
|
help="path where to write output json file",
|
||||||
|
type=click.Path())
|
||||||
|
def _get_ocio_config_views(config_path, output_path):
|
||||||
"""Aggregate all viewers to file.
|
"""Aggregate all viewers to file.
|
||||||
|
|
||||||
Python 2 wrapped console command
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
in_path (str): config file path string
|
config_path (str): config file path string
|
||||||
out_path (str): temp json file path string
|
output_path (str): temp json file path string
|
||||||
|
|
||||||
Example of use:
|
Example of use:
|
||||||
> pyton.exe ./ocio_wrapper.py config get_views \
|
> pyton.exe ./ocio_wrapper.py config get_views \
|
||||||
--in_path=<path> --out_path=<path>
|
--config_path <path> --output <path>
|
||||||
"""
|
"""
|
||||||
json_path = Path(out_path)
|
_save_output_to_json_file(
|
||||||
|
get_ocio_config_views(config_path),
|
||||||
out_data = _get_views_data(in_path)
|
output_path
|
||||||
|
|
||||||
with open(json_path, "w") as f_:
|
|
||||||
json.dump(out_data, f_)
|
|
||||||
|
|
||||||
print(f"Viewer data are saved to '{json_path}'")
|
|
||||||
|
|
||||||
|
|
||||||
def _get_views_data(config_path):
|
|
||||||
"""Return all found viewer data.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
config_path (str): path string leading to config.ocio
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
IOError: Input config does not exist.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: aggregated available viewers
|
|
||||||
"""
|
|
||||||
config_path = Path(config_path)
|
|
||||||
|
|
||||||
if not config_path.is_file():
|
|
||||||
raise IOError("Input path should be `config.ocio` file")
|
|
||||||
|
|
||||||
config = ocio.Config().CreateFromFile(str(config_path))
|
|
||||||
|
|
||||||
data_ = {}
|
|
||||||
for display in config.getDisplays():
|
|
||||||
for view in config.getViews(display):
|
|
||||||
colorspace = config.getDisplayViewColorSpaceName(display, view)
|
|
||||||
# Special token. See https://opencolorio.readthedocs.io/en/latest/guides/authoring/authoring.html#shared-views # noqa
|
|
||||||
if colorspace == "<USE_DISPLAY_NAME>":
|
|
||||||
colorspace = display
|
|
||||||
|
|
||||||
data_[f"{display}/{view}"] = {
|
|
||||||
"display": display,
|
|
||||||
"view": view,
|
|
||||||
"colorspace": colorspace
|
|
||||||
}
|
|
||||||
|
|
||||||
return data_
|
|
||||||
|
|
||||||
|
|
||||||
@config.command(
|
|
||||||
name="get_version",
|
|
||||||
help=(
|
|
||||||
"return major and minor version from config file "
|
|
||||||
"--config_path input arg is required"
|
|
||||||
"--out_path input arg is required"
|
|
||||||
)
|
)
|
||||||
)
|
|
||||||
@click.option("--config_path", required=True,
|
|
||||||
help="path where to read ocio config file",
|
|
||||||
type=click.Path(exists=True))
|
|
||||||
@click.option("--out_path", required=True,
|
|
||||||
help="path where to write output json file",
|
|
||||||
type=click.Path())
|
|
||||||
def get_version(config_path, out_path):
|
|
||||||
"""Get version of config.
|
|
||||||
|
|
||||||
Python 2 wrapped console command
|
|
||||||
|
@main.command(
|
||||||
|
name="get_config_version_data",
|
||||||
|
help="Get major and minor version from config file")
|
||||||
|
@click.option(
|
||||||
|
"--config_path",
|
||||||
|
required=True,
|
||||||
|
help="OCIO config path to read ocio config file.",
|
||||||
|
type=click.Path(exists=True))
|
||||||
|
@click.option(
|
||||||
|
"--output_path",
|
||||||
|
required=True,
|
||||||
|
help="path where to write output json file",
|
||||||
|
type=click.Path())
|
||||||
|
def _get_config_version_data(config_path, output_path):
|
||||||
|
"""Get version of config.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
config_path (str): ocio config file path string
|
config_path (str): ocio config file path string
|
||||||
out_path (str): temp json file path string
|
output_path (str): temp json file path string
|
||||||
|
|
||||||
Example of use:
|
Example of use:
|
||||||
> pyton.exe ./ocio_wrapper.py config get_version \
|
> pyton.exe ./ocio_wrapper.py config get_version \
|
||||||
--config_path=<path> --out_path=<path>
|
--config_path <path> --output_path <path>
|
||||||
"""
|
"""
|
||||||
json_path = Path(out_path)
|
_save_output_to_json_file(
|
||||||
|
get_config_version_data(config_path),
|
||||||
out_data = _get_version_data(config_path)
|
output_path
|
||||||
|
|
||||||
with open(json_path, "w") as f_:
|
|
||||||
json.dump(out_data, f_)
|
|
||||||
|
|
||||||
print(f"Config version data are saved to '{json_path}'")
|
|
||||||
|
|
||||||
|
|
||||||
def _get_version_data(config_path):
|
|
||||||
"""Return major and minor version info.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
config_path (str): path string leading to config.ocio
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
IOError: Input config does not exist.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: minor and major keys with values
|
|
||||||
"""
|
|
||||||
config_path = Path(config_path)
|
|
||||||
|
|
||||||
if not config_path.is_file():
|
|
||||||
raise IOError("Input path should be `config.ocio` file")
|
|
||||||
|
|
||||||
config = ocio.Config().CreateFromFile(str(config_path))
|
|
||||||
|
|
||||||
return {
|
|
||||||
"major": config.getMajorVersion(),
|
|
||||||
"minor": config.getMinorVersion()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@colorspace.command(
|
|
||||||
name="get_config_file_rules_colorspace_from_filepath",
|
|
||||||
help=(
|
|
||||||
"return colorspace from filepath "
|
|
||||||
"--config_path - ocio config file path (input arg is required) "
|
|
||||||
"--filepath - any file path (input arg is required) "
|
|
||||||
"--out_path - temp json file path (input arg is required)"
|
|
||||||
)
|
)
|
||||||
)
|
|
||||||
@click.option("--config_path", required=True,
|
|
||||||
help="path where to read ocio config file",
|
@main.command(
|
||||||
type=click.Path(exists=True))
|
name="get_config_file_rules_colorspace_from_filepath",
|
||||||
@click.option("--filepath", required=True,
|
help="Colorspace file rules from filepath")
|
||||||
help="path to file to get colorspace from",
|
@click.option(
|
||||||
type=click.Path())
|
"--config_path",
|
||||||
@click.option("--out_path", required=True,
|
required=True,
|
||||||
help="path where to write output json file",
|
help="OCIO config path to read ocio config file.",
|
||||||
type=click.Path())
|
type=click.Path(exists=True))
|
||||||
def get_config_file_rules_colorspace_from_filepath(
|
@click.option(
|
||||||
config_path, filepath, out_path
|
"--filepath",
|
||||||
|
required=True,
|
||||||
|
help="Path to file to get colorspace from.",
|
||||||
|
type=click.Path())
|
||||||
|
@click.option(
|
||||||
|
"--output_path",
|
||||||
|
required=True,
|
||||||
|
help="Path where to write output json file.",
|
||||||
|
type=click.Path())
|
||||||
|
def _get_config_file_rules_colorspace_from_filepath(
|
||||||
|
config_path, filepath, output_path
|
||||||
):
|
):
|
||||||
"""Get colorspace from file path wrapper.
|
"""Get colorspace from file path wrapper.
|
||||||
|
|
||||||
Python 2 wrapped console command
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
config_path (str): config file path string
|
config_path (str): config file path string
|
||||||
filepath (str): path string leading to file
|
filepath (str): path string leading to file
|
||||||
out_path (str): temp json file path string
|
output_path (str): temp json file path string
|
||||||
|
|
||||||
Example of use:
|
Example of use:
|
||||||
> pyton.exe ./ocio_wrapper.py \
|
> python.exe ./ocio_wrapper.py \
|
||||||
colorspace get_config_file_rules_colorspace_from_filepath \
|
colorspace get_config_file_rules_colorspace_from_filepath \
|
||||||
--config_path=<path> --filepath=<path> --out_path=<path>
|
--config_path <path> --filepath <path> --output_path <path>
|
||||||
"""
|
"""
|
||||||
json_path = Path(out_path)
|
_save_output_to_json_file(
|
||||||
|
get_config_file_rules_colorspace_from_filepath(config_path, filepath),
|
||||||
colorspace = _get_config_file_rules_colorspace_from_filepath(
|
output_path
|
||||||
config_path, filepath)
|
)
|
||||||
|
|
||||||
with open(json_path, "w") as f_:
|
|
||||||
json.dump(colorspace, f_)
|
|
||||||
|
|
||||||
print(f"Colorspace name is saved to '{json_path}'")
|
|
||||||
|
|
||||||
|
|
||||||
def _get_config_file_rules_colorspace_from_filepath(config_path, filepath):
|
@main.command(
|
||||||
"""Return found colorspace data found in v2 file rules.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
config_path (str): path string leading to config.ocio
|
|
||||||
filepath (str): path string leading to v2 file rules
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
IOError: Input config does not exist.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: aggregated available colorspaces
|
|
||||||
"""
|
|
||||||
config_path = Path(config_path)
|
|
||||||
|
|
||||||
if not config_path.is_file():
|
|
||||||
raise IOError(
|
|
||||||
f"Input path `{config_path}` should be `config.ocio` file")
|
|
||||||
|
|
||||||
config = ocio.Config().CreateFromFile(str(config_path))
|
|
||||||
|
|
||||||
# TODO: use `parseColorSpaceFromString` instead if ocio v1
|
|
||||||
colorspace = config.getColorSpaceFromFilepath(str(filepath))
|
|
||||||
|
|
||||||
return colorspace
|
|
||||||
|
|
||||||
|
|
||||||
def _get_display_view_colorspace_name(config_path, display, view):
|
|
||||||
"""Returns the colorspace attribute of the (display, view) pair.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
config_path (str): path string leading to config.ocio
|
|
||||||
display (str): display name e.g. "ACES"
|
|
||||||
view (str): view name e.g. "sRGB"
|
|
||||||
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
IOError: Input config does not exist.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
view color space name (str) e.g. "Output - sRGB"
|
|
||||||
"""
|
|
||||||
|
|
||||||
config_path = Path(config_path)
|
|
||||||
|
|
||||||
if not config_path.is_file():
|
|
||||||
raise IOError("Input path should be `config.ocio` file")
|
|
||||||
|
|
||||||
config = ocio.Config.CreateFromFile(str(config_path))
|
|
||||||
colorspace = config.getDisplayViewColorSpaceName(display, view)
|
|
||||||
|
|
||||||
return colorspace
|
|
||||||
|
|
||||||
|
|
||||||
@config.command(
|
|
||||||
name="get_display_view_colorspace_name",
|
name="get_display_view_colorspace_name",
|
||||||
help=(
|
help=(
|
||||||
"return default view colorspace name "
|
"Default view colorspace name for the given display and view"
|
||||||
"for the given display and view "
|
))
|
||||||
"--path input arg is required"
|
@click.option(
|
||||||
)
|
"--config_path",
|
||||||
)
|
required=True,
|
||||||
@click.option("--in_path", required=True,
|
help="path where to read ocio config file",
|
||||||
help="path where to read ocio config file",
|
type=click.Path(exists=True))
|
||||||
type=click.Path(exists=True))
|
@click.option(
|
||||||
@click.option("--out_path", required=True,
|
"--display",
|
||||||
help="path where to write output json file",
|
required=True,
|
||||||
type=click.Path())
|
help="Display name",
|
||||||
@click.option("--display", required=True,
|
type=click.STRING)
|
||||||
help="display name",
|
@click.option(
|
||||||
type=click.STRING)
|
"--view",
|
||||||
@click.option("--view", required=True,
|
required=True,
|
||||||
help="view name",
|
help="view name",
|
||||||
type=click.STRING)
|
type=click.STRING)
|
||||||
def get_display_view_colorspace_name(in_path, out_path,
|
@click.option(
|
||||||
display, view):
|
"--output_path",
|
||||||
|
required=True,
|
||||||
|
help="path where to write output json file",
|
||||||
|
type=click.Path())
|
||||||
|
def _get_display_view_colorspace_name(
|
||||||
|
config_path, display, view, output_path
|
||||||
|
):
|
||||||
"""Aggregate view colorspace name to file.
|
"""Aggregate view colorspace name to file.
|
||||||
|
|
||||||
Wrapper command for processes without access to OpenColorIO
|
Wrapper command for processes without access to OpenColorIO
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
in_path (str): config file path string
|
config_path (str): config file path string
|
||||||
out_path (str): temp json file path string
|
output_path (str): temp json file path string
|
||||||
display (str): display name e.g. "ACES"
|
display (str): display name e.g. "ACES"
|
||||||
view (str): view name e.g. "sRGB"
|
view (str): view name e.g. "sRGB"
|
||||||
|
|
||||||
Example of use:
|
Example of use:
|
||||||
> pyton.exe ./ocio_wrapper.py config \
|
> pyton.exe ./ocio_wrapper.py config \
|
||||||
get_display_view_colorspace_name --in_path=<path> \
|
get_display_view_colorspace_name --config_path <path> \
|
||||||
--out_path=<path> --display=<display> --view=<view>
|
--output_path <path> --display <display> --view <view>
|
||||||
"""
|
"""
|
||||||
|
_save_output_to_json_file(
|
||||||
|
get_display_view_colorspace_name(config_path, display, view),
|
||||||
|
output_path
|
||||||
|
)
|
||||||
|
|
||||||
out_data = _get_display_view_colorspace_name(in_path,
|
|
||||||
display,
|
|
||||||
view)
|
|
||||||
|
|
||||||
with open(out_path, "w") as f:
|
if __name__ == "__main__":
|
||||||
json.dump(out_data, f)
|
if not has_compatible_ocio_package():
|
||||||
|
raise RuntimeError("OpenColorIO is not available.")
|
||||||
print(f"Display view colorspace saved to '{out_path}'")
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
main()
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ from .hierarchy import (
|
||||||
)
|
)
|
||||||
from .thumbnails import ThumbnailsModel
|
from .thumbnails import ThumbnailsModel
|
||||||
from .selection import HierarchyExpectedSelection
|
from .selection import HierarchyExpectedSelection
|
||||||
|
from .users import UsersModel
|
||||||
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
|
@ -32,4 +33,6 @@ __all__ = (
|
||||||
"ThumbnailsModel",
|
"ThumbnailsModel",
|
||||||
|
|
||||||
"HierarchyExpectedSelection",
|
"HierarchyExpectedSelection",
|
||||||
|
|
||||||
|
"UsersModel",
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import ayon_api
|
||||||
import six
|
import six
|
||||||
|
|
||||||
from ayon_core.style import get_default_entity_icon_color
|
from ayon_core.style import get_default_entity_icon_color
|
||||||
from ayon_core.lib import CacheItem
|
from ayon_core.lib import CacheItem, NestedCacheItem
|
||||||
|
|
||||||
PROJECTS_MODEL_SENDER = "projects.model"
|
PROJECTS_MODEL_SENDER = "projects.model"
|
||||||
|
|
||||||
|
|
@ -17,6 +17,49 @@ class AbstractHierarchyController:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class StatusItem:
|
||||||
|
"""Item representing status of project.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): Status name ("Not ready").
|
||||||
|
color (str): Status color in hex ("#434a56").
|
||||||
|
short (str): Short status name ("NRD").
|
||||||
|
icon (str): Icon name in MaterialIcons ("fiber_new").
|
||||||
|
state (Literal["not_started", "in_progress", "done", "blocked"]):
|
||||||
|
Status state.
|
||||||
|
|
||||||
|
"""
|
||||||
|
def __init__(self, name, color, short, icon, state):
|
||||||
|
self.name = name
|
||||||
|
self.color = color
|
||||||
|
self.short = short
|
||||||
|
self.icon = icon
|
||||||
|
self.state = state
|
||||||
|
|
||||||
|
def to_data(self):
|
||||||
|
return {
|
||||||
|
"name": self.name,
|
||||||
|
"color": self.color,
|
||||||
|
"short": self.short,
|
||||||
|
"icon": self.icon,
|
||||||
|
"state": self.state,
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_data(cls, data):
|
||||||
|
return cls(**data)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_project_item(cls, status_data):
|
||||||
|
return cls(
|
||||||
|
name=status_data["name"],
|
||||||
|
color=status_data["color"],
|
||||||
|
short=status_data["shortName"],
|
||||||
|
icon=status_data["icon"],
|
||||||
|
state=status_data["state"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ProjectItem:
|
class ProjectItem:
|
||||||
"""Item representing folder entity on a server.
|
"""Item representing folder entity on a server.
|
||||||
|
|
||||||
|
|
@ -40,6 +83,23 @@ class ProjectItem:
|
||||||
}
|
}
|
||||||
self.icon = icon
|
self.icon = icon
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_entity(cls, project_entity):
|
||||||
|
"""Creates folder item from entity.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
project_entity (dict[str, Any]): Project entity.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ProjectItem: Project item.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return cls(
|
||||||
|
project_entity["name"],
|
||||||
|
project_entity["active"],
|
||||||
|
project_entity["library"],
|
||||||
|
)
|
||||||
|
|
||||||
def to_data(self):
|
def to_data(self):
|
||||||
"""Converts folder item to data.
|
"""Converts folder item to data.
|
||||||
|
|
||||||
|
|
@ -79,7 +139,7 @@ def _get_project_items_from_entitiy(projects):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return [
|
return [
|
||||||
ProjectItem(project["name"], project["active"], project["library"])
|
ProjectItem.from_entity(project)
|
||||||
for project in projects
|
for project in projects
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -87,18 +147,29 @@ def _get_project_items_from_entitiy(projects):
|
||||||
class ProjectsModel(object):
|
class ProjectsModel(object):
|
||||||
def __init__(self, controller):
|
def __init__(self, controller):
|
||||||
self._projects_cache = CacheItem(default_factory=list)
|
self._projects_cache = CacheItem(default_factory=list)
|
||||||
self._project_items_by_name = {}
|
self._project_statuses_cache = NestedCacheItem(
|
||||||
self._projects_by_name = {}
|
levels=1, default_factory=list
|
||||||
|
)
|
||||||
|
self._projects_by_name = NestedCacheItem(
|
||||||
|
levels=1, default_factory=list
|
||||||
|
)
|
||||||
|
|
||||||
self._is_refreshing = False
|
self._is_refreshing = False
|
||||||
self._controller = controller
|
self._controller = controller
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
self._projects_cache.reset()
|
self._projects_cache.reset()
|
||||||
self._project_items_by_name = {}
|
self._project_statuses_cache.reset()
|
||||||
self._projects_by_name = {}
|
self._projects_by_name.reset()
|
||||||
|
|
||||||
def refresh(self):
|
def refresh(self):
|
||||||
|
"""Refresh project items.
|
||||||
|
|
||||||
|
This method will requery list of ProjectItem returned by
|
||||||
|
'get_project_items'.
|
||||||
|
|
||||||
|
To reset all cached items use 'reset' method.
|
||||||
|
"""
|
||||||
self._refresh_projects_cache()
|
self._refresh_projects_cache()
|
||||||
|
|
||||||
def get_project_items(self, sender):
|
def get_project_items(self, sender):
|
||||||
|
|
@ -117,12 +188,51 @@ class ProjectsModel(object):
|
||||||
return self._projects_cache.get_data()
|
return self._projects_cache.get_data()
|
||||||
|
|
||||||
def get_project_entity(self, project_name):
|
def get_project_entity(self, project_name):
|
||||||
if project_name not in self._projects_by_name:
|
"""Get project entity.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
project_name (str): Project name.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Union[dict[str, Any], None]: Project entity or None if project
|
||||||
|
was not found by name.
|
||||||
|
|
||||||
|
"""
|
||||||
|
project_cache = self._projects_by_name[project_name]
|
||||||
|
if not project_cache.is_valid:
|
||||||
entity = None
|
entity = None
|
||||||
if project_name:
|
if project_name:
|
||||||
entity = ayon_api.get_project(project_name)
|
entity = ayon_api.get_project(project_name)
|
||||||
self._projects_by_name[project_name] = entity
|
project_cache.update_data(entity)
|
||||||
return self._projects_by_name[project_name]
|
return project_cache.get_data()
|
||||||
|
|
||||||
|
def get_project_status_items(self, project_name, sender):
|
||||||
|
"""Get project status items.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
project_name (str): Project name.
|
||||||
|
sender (Union[str, None]): Name of sender who asked for items.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[StatusItem]: Status items for project.
|
||||||
|
|
||||||
|
"""
|
||||||
|
statuses_cache = self._project_statuses_cache[project_name]
|
||||||
|
if not statuses_cache.is_valid:
|
||||||
|
with self._project_statuses_refresh_event_manager(
|
||||||
|
sender, project_name
|
||||||
|
):
|
||||||
|
project_entity = None
|
||||||
|
if project_name:
|
||||||
|
project_entity = self.get_project_entity(project_name)
|
||||||
|
statuses = []
|
||||||
|
if project_entity:
|
||||||
|
statuses = [
|
||||||
|
StatusItem.from_project_item(status)
|
||||||
|
for status in project_entity["statuses"]
|
||||||
|
]
|
||||||
|
statuses_cache.update_data(statuses)
|
||||||
|
return statuses_cache.get_data()
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def _project_refresh_event_manager(self, sender):
|
def _project_refresh_event_manager(self, sender):
|
||||||
|
|
@ -143,6 +253,23 @@ class ProjectsModel(object):
|
||||||
)
|
)
|
||||||
self._is_refreshing = False
|
self._is_refreshing = False
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def _project_statuses_refresh_event_manager(self, sender, project_name):
|
||||||
|
self._controller.emit_event(
|
||||||
|
"projects.statuses.refresh.started",
|
||||||
|
{"sender": sender, "project_name": project_name},
|
||||||
|
PROJECTS_MODEL_SENDER
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
|
||||||
|
finally:
|
||||||
|
self._controller.emit_event(
|
||||||
|
"projects.statuses.refresh.finished",
|
||||||
|
{"sender": sender, "project_name": project_name},
|
||||||
|
PROJECTS_MODEL_SENDER
|
||||||
|
)
|
||||||
|
|
||||||
def _refresh_projects_cache(self, sender=None):
|
def _refresh_projects_cache(self, sender=None):
|
||||||
if self._is_refreshing:
|
if self._is_refreshing:
|
||||||
return None
|
return None
|
||||||
|
|
|
||||||
84
client/ayon_core/tools/common_models/users.py
Normal file
84
client/ayon_core/tools/common_models/users.py
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
import ayon_api
|
||||||
|
|
||||||
|
from ayon_core.lib import CacheItem
|
||||||
|
|
||||||
|
|
||||||
|
class UserItem:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
username,
|
||||||
|
full_name,
|
||||||
|
email,
|
||||||
|
avatar_url,
|
||||||
|
active,
|
||||||
|
):
|
||||||
|
self.username = username
|
||||||
|
self.full_name = full_name
|
||||||
|
self.email = email
|
||||||
|
self.avatar_url = avatar_url
|
||||||
|
self.active = active
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_entity_data(cls, user_data):
|
||||||
|
return cls(
|
||||||
|
user_data["name"],
|
||||||
|
user_data["attrib"]["fullName"],
|
||||||
|
user_data["attrib"]["email"],
|
||||||
|
user_data["attrib"]["avatarUrl"],
|
||||||
|
user_data["active"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class UsersModel:
|
||||||
|
def __init__(self, controller):
|
||||||
|
self._controller = controller
|
||||||
|
self._users_cache = CacheItem(default_factory=list)
|
||||||
|
|
||||||
|
def get_user_items(self):
|
||||||
|
"""Get user items.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[UserItem]: List of user items.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self._invalidate_cache()
|
||||||
|
return self._users_cache.get_data()
|
||||||
|
|
||||||
|
def get_user_items_by_name(self):
|
||||||
|
"""Get user items by name.
|
||||||
|
|
||||||
|
Implemented as most of cases using this model will need to find
|
||||||
|
user information by username.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict[str, UserItem]: Dictionary of user items by name.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
user_item.username: user_item
|
||||||
|
for user_item in self.get_user_items()
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_user_item_by_username(self, username):
|
||||||
|
"""Get user item by username.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
username (str): Username.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Union[UserItem, None]: User item or None if not found.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self._invalidate_cache()
|
||||||
|
for user_item in self.get_user_items():
|
||||||
|
if user_item.username == username:
|
||||||
|
return user_item
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _invalidate_cache(self):
|
||||||
|
if self._users_cache.is_valid:
|
||||||
|
return
|
||||||
|
self._users_cache.update_data([
|
||||||
|
UserItem.from_entity_data(user)
|
||||||
|
for user in ayon_api.get_users()
|
||||||
|
])
|
||||||
|
|
@ -290,6 +290,34 @@ class ActionDelegate(QtWidgets.QStyledItemDelegate):
|
||||||
painter.drawPixmap(extender_x, extender_y, pix)
|
painter.drawPixmap(extender_x, extender_y, pix)
|
||||||
|
|
||||||
|
|
||||||
|
class ActionsProxyModel(QtCore.QSortFilterProxyModel):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)
|
||||||
|
|
||||||
|
def lessThan(self, left, right):
|
||||||
|
# Sort by action order and then by label
|
||||||
|
left_value = left.data(ACTION_SORT_ROLE)
|
||||||
|
right_value = right.data(ACTION_SORT_ROLE)
|
||||||
|
|
||||||
|
# Values are same -> use super sorting
|
||||||
|
if left_value == right_value:
|
||||||
|
# Default behavior is using DisplayRole
|
||||||
|
return super().lessThan(left, right)
|
||||||
|
|
||||||
|
# Validate 'None' values
|
||||||
|
if right_value is None:
|
||||||
|
return True
|
||||||
|
if left_value is None:
|
||||||
|
return False
|
||||||
|
# Sort values and handle incompatible types
|
||||||
|
try:
|
||||||
|
return left_value < right_value
|
||||||
|
except TypeError:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
class ActionsWidget(QtWidgets.QWidget):
|
class ActionsWidget(QtWidgets.QWidget):
|
||||||
def __init__(self, controller, parent):
|
def __init__(self, controller, parent):
|
||||||
super(ActionsWidget, self).__init__(parent)
|
super(ActionsWidget, self).__init__(parent)
|
||||||
|
|
@ -316,10 +344,7 @@ class ActionsWidget(QtWidgets.QWidget):
|
||||||
|
|
||||||
model = ActionsQtModel(controller)
|
model = ActionsQtModel(controller)
|
||||||
|
|
||||||
proxy_model = QtCore.QSortFilterProxyModel()
|
proxy_model = ActionsProxyModel()
|
||||||
proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)
|
|
||||||
proxy_model.setSortRole(ACTION_SORT_ROLE)
|
|
||||||
|
|
||||||
proxy_model.setSourceModel(model)
|
proxy_model.setSourceModel(model)
|
||||||
view.setModel(proxy_model)
|
view.setModel(proxy_model)
|
||||||
|
|
||||||
|
|
@ -359,7 +384,8 @@ class ActionsWidget(QtWidgets.QWidget):
|
||||||
def _on_model_refresh(self):
|
def _on_model_refresh(self):
|
||||||
self._proxy_model.sort(0)
|
self._proxy_model.sort(0)
|
||||||
# Force repaint all items
|
# Force repaint all items
|
||||||
self._view.update()
|
viewport = self._view.viewport()
|
||||||
|
viewport.update()
|
||||||
|
|
||||||
def _on_animation(self):
|
def _on_animation(self):
|
||||||
time_now = time.time()
|
time_now = time.time()
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,7 @@ class VersionItem:
|
||||||
thumbnail_id (Union[str, None]): Thumbnail id.
|
thumbnail_id (Union[str, None]): Thumbnail id.
|
||||||
published_time (Union[str, None]): Published time in format
|
published_time (Union[str, None]): Published time in format
|
||||||
'%Y%m%dT%H%M%SZ'.
|
'%Y%m%dT%H%M%SZ'.
|
||||||
|
status (Union[str, None]): Status name.
|
||||||
author (Union[str, None]): Author.
|
author (Union[str, None]): Author.
|
||||||
frame_range (Union[str, None]): Frame range.
|
frame_range (Union[str, None]): Frame range.
|
||||||
duration (Union[int, None]): Duration.
|
duration (Union[int, None]): Duration.
|
||||||
|
|
@ -132,6 +133,7 @@ class VersionItem:
|
||||||
thumbnail_id,
|
thumbnail_id,
|
||||||
published_time,
|
published_time,
|
||||||
author,
|
author,
|
||||||
|
status,
|
||||||
frame_range,
|
frame_range,
|
||||||
duration,
|
duration,
|
||||||
handles,
|
handles,
|
||||||
|
|
@ -146,6 +148,7 @@ class VersionItem:
|
||||||
self.is_hero = is_hero
|
self.is_hero = is_hero
|
||||||
self.published_time = published_time
|
self.published_time = published_time
|
||||||
self.author = author
|
self.author = author
|
||||||
|
self.status = status
|
||||||
self.frame_range = frame_range
|
self.frame_range = frame_range
|
||||||
self.duration = duration
|
self.duration = duration
|
||||||
self.handles = handles
|
self.handles = handles
|
||||||
|
|
@ -169,12 +172,30 @@ class VersionItem:
|
||||||
def __gt__(self, other):
|
def __gt__(self, other):
|
||||||
if not isinstance(other, VersionItem):
|
if not isinstance(other, VersionItem):
|
||||||
return False
|
return False
|
||||||
if (
|
# Make sure hero versions are positive
|
||||||
other.version == self.version
|
version = abs(self.version)
|
||||||
and self.is_hero
|
other_version = abs(other.version)
|
||||||
):
|
# Hero version is greater than non-hero
|
||||||
|
if version == other_version:
|
||||||
|
return self.is_hero
|
||||||
|
return version > other_version
|
||||||
|
|
||||||
|
def __lt__(self, other):
|
||||||
|
if not isinstance(other, VersionItem):
|
||||||
return True
|
return True
|
||||||
return other.version < self.version
|
# Make sure hero versions are positive
|
||||||
|
version = abs(self.version)
|
||||||
|
other_version = abs(other.version)
|
||||||
|
# Non-hero version is lesser than hero
|
||||||
|
if version == other_version:
|
||||||
|
return not self.is_hero
|
||||||
|
return version < other_version
|
||||||
|
|
||||||
|
def __ge__(self, other):
|
||||||
|
return self.__eq__(other) or self.__gt__(other)
|
||||||
|
|
||||||
|
def __le__(self, other):
|
||||||
|
return self.__eq__(other) or self.__lt__(other)
|
||||||
|
|
||||||
def to_data(self):
|
def to_data(self):
|
||||||
return {
|
return {
|
||||||
|
|
@ -185,6 +206,7 @@ class VersionItem:
|
||||||
"is_hero": self.is_hero,
|
"is_hero": self.is_hero,
|
||||||
"published_time": self.published_time,
|
"published_time": self.published_time,
|
||||||
"author": self.author,
|
"author": self.author,
|
||||||
|
"status": self.status,
|
||||||
"frame_range": self.frame_range,
|
"frame_range": self.frame_range,
|
||||||
"duration": self.duration,
|
"duration": self.duration,
|
||||||
"handles": self.handles,
|
"handles": self.handles,
|
||||||
|
|
@ -488,6 +510,27 @@ class FrontendLoaderController(_BaseLoaderController):
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_project_status_items(self, project_name, sender=None):
|
||||||
|
"""Items for all projects available on server.
|
||||||
|
|
||||||
|
Triggers event topics "projects.statuses.refresh.started" and
|
||||||
|
"projects.statuses.refresh.finished" with data:
|
||||||
|
{
|
||||||
|
"sender": sender,
|
||||||
|
"project_name": project_name
|
||||||
|
}
|
||||||
|
|
||||||
|
Args:
|
||||||
|
project_name (Union[str, None]): Project name.
|
||||||
|
sender (Optional[str]): Sender who requested the items.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[StatusItem]: List of status items.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_product_items(self, project_name, folder_ids, sender=None):
|
def get_product_items(self, project_name, folder_ids, sender=None):
|
||||||
"""Product items for folder ids.
|
"""Product items for folder ids.
|
||||||
|
|
|
||||||
|
|
@ -180,6 +180,11 @@ class LoaderController(BackendLoaderController, FrontendLoaderController):
|
||||||
def get_project_items(self, sender=None):
|
def get_project_items(self, sender=None):
|
||||||
return self._projects_model.get_project_items(sender)
|
return self._projects_model.get_project_items(sender)
|
||||||
|
|
||||||
|
def get_project_status_items(self, project_name, sender=None):
|
||||||
|
return self._projects_model.get_project_status_items(
|
||||||
|
project_name, sender
|
||||||
|
)
|
||||||
|
|
||||||
def get_folder_items(self, project_name, sender=None):
|
def get_folder_items(self, project_name, sender=None):
|
||||||
return self._hierarchy_model.get_folder_items(project_name, sender)
|
return self._hierarchy_model.get_folder_items(project_name, sender)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@ def version_item_from_entity(version):
|
||||||
thumbnail_id=version["thumbnailId"],
|
thumbnail_id=version["thumbnailId"],
|
||||||
published_time=published_time,
|
published_time=published_time,
|
||||||
author=author,
|
author=author,
|
||||||
|
status=version["status"],
|
||||||
frame_range=frame_range,
|
frame_range=frame_range,
|
||||||
duration=duration,
|
duration=duration,
|
||||||
handles=handles,
|
handles=handles,
|
||||||
|
|
@ -526,8 +527,11 @@ class ProductsModel:
|
||||||
products = list(ayon_api.get_products(project_name, **kwargs))
|
products = list(ayon_api.get_products(project_name, **kwargs))
|
||||||
product_ids = {product["id"] for product in products}
|
product_ids = {product["id"] for product in products}
|
||||||
|
|
||||||
|
# Add 'status' to fields -> fixed in ayon-python-api 1.0.4
|
||||||
|
fields = ayon_api.get_default_fields_for_type("version")
|
||||||
|
fields.add("status")
|
||||||
versions = ayon_api.get_versions(
|
versions = ayon_api.get_versions(
|
||||||
project_name, product_ids=product_ids
|
project_name, product_ids=product_ids, fields=fields
|
||||||
)
|
)
|
||||||
|
|
||||||
return self._create_product_items(
|
return self._create_product_items(
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,10 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate):
|
||||||
style = QtWidgets.QApplication.style()
|
style = QtWidgets.QApplication.style()
|
||||||
|
|
||||||
style.drawControl(
|
style.drawControl(
|
||||||
style.CE_ItemViewItem, option, painter, option.widget
|
QtWidgets.QCommonStyle.CE_ItemViewItem,
|
||||||
|
option,
|
||||||
|
painter,
|
||||||
|
option.widget
|
||||||
)
|
)
|
||||||
|
|
||||||
painter.save()
|
painter.save()
|
||||||
|
|
@ -116,9 +119,14 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate):
|
||||||
pen.setColor(fg_color)
|
pen.setColor(fg_color)
|
||||||
painter.setPen(pen)
|
painter.setPen(pen)
|
||||||
|
|
||||||
text_rect = style.subElementRect(style.SE_ItemViewItemText, option)
|
text_rect = style.subElementRect(
|
||||||
|
QtWidgets.QCommonStyle.SE_ItemViewItemText,
|
||||||
|
option
|
||||||
|
)
|
||||||
text_margin = style.proxy().pixelMetric(
|
text_margin = style.proxy().pixelMetric(
|
||||||
style.PM_FocusFrameHMargin, option, option.widget
|
QtWidgets.QCommonStyle.PM_FocusFrameHMargin,
|
||||||
|
option,
|
||||||
|
option.widget
|
||||||
) + 1
|
) + 1
|
||||||
|
|
||||||
painter.drawText(
|
painter.drawText(
|
||||||
|
|
|
||||||
|
|
@ -22,18 +22,22 @@ VERSION_HERO_ROLE = QtCore.Qt.UserRole + 11
|
||||||
VERSION_NAME_ROLE = QtCore.Qt.UserRole + 12
|
VERSION_NAME_ROLE = QtCore.Qt.UserRole + 12
|
||||||
VERSION_NAME_EDIT_ROLE = QtCore.Qt.UserRole + 13
|
VERSION_NAME_EDIT_ROLE = QtCore.Qt.UserRole + 13
|
||||||
VERSION_PUBLISH_TIME_ROLE = QtCore.Qt.UserRole + 14
|
VERSION_PUBLISH_TIME_ROLE = QtCore.Qt.UserRole + 14
|
||||||
VERSION_AUTHOR_ROLE = QtCore.Qt.UserRole + 15
|
VERSION_STATUS_NAME_ROLE = QtCore.Qt.UserRole + 15
|
||||||
VERSION_FRAME_RANGE_ROLE = QtCore.Qt.UserRole + 16
|
VERSION_STATUS_SHORT_ROLE = QtCore.Qt.UserRole + 16
|
||||||
VERSION_DURATION_ROLE = QtCore.Qt.UserRole + 17
|
VERSION_STATUS_COLOR_ROLE = QtCore.Qt.UserRole + 17
|
||||||
VERSION_HANDLES_ROLE = QtCore.Qt.UserRole + 18
|
VERSION_STATUS_ICON_ROLE = QtCore.Qt.UserRole + 18
|
||||||
VERSION_STEP_ROLE = QtCore.Qt.UserRole + 19
|
VERSION_AUTHOR_ROLE = QtCore.Qt.UserRole + 19
|
||||||
VERSION_AVAILABLE_ROLE = QtCore.Qt.UserRole + 20
|
VERSION_FRAME_RANGE_ROLE = QtCore.Qt.UserRole + 20
|
||||||
VERSION_THUMBNAIL_ID_ROLE = QtCore.Qt.UserRole + 21
|
VERSION_DURATION_ROLE = QtCore.Qt.UserRole + 21
|
||||||
ACTIVE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 22
|
VERSION_HANDLES_ROLE = QtCore.Qt.UserRole + 22
|
||||||
REMOTE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 23
|
VERSION_STEP_ROLE = QtCore.Qt.UserRole + 23
|
||||||
REPRESENTATIONS_COUNT_ROLE = QtCore.Qt.UserRole + 24
|
VERSION_AVAILABLE_ROLE = QtCore.Qt.UserRole + 24
|
||||||
SYNC_ACTIVE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 25
|
VERSION_THUMBNAIL_ID_ROLE = QtCore.Qt.UserRole + 25
|
||||||
SYNC_REMOTE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 26
|
ACTIVE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 26
|
||||||
|
REMOTE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 27
|
||||||
|
REPRESENTATIONS_COUNT_ROLE = QtCore.Qt.UserRole + 28
|
||||||
|
SYNC_ACTIVE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 29
|
||||||
|
SYNC_REMOTE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 30
|
||||||
|
|
||||||
|
|
||||||
class ProductsModel(QtGui.QStandardItemModel):
|
class ProductsModel(QtGui.QStandardItemModel):
|
||||||
|
|
@ -44,6 +48,7 @@ class ProductsModel(QtGui.QStandardItemModel):
|
||||||
"Product type",
|
"Product type",
|
||||||
"Folder",
|
"Folder",
|
||||||
"Version",
|
"Version",
|
||||||
|
"Status",
|
||||||
"Time",
|
"Time",
|
||||||
"Author",
|
"Author",
|
||||||
"Frames",
|
"Frames",
|
||||||
|
|
@ -69,11 +74,35 @@ class ProductsModel(QtGui.QStandardItemModel):
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
version_col = column_labels.index("Version")
|
product_name_col = column_labels.index("Product name")
|
||||||
published_time_col = column_labels.index("Time")
|
product_type_col = column_labels.index("Product type")
|
||||||
folders_label_col = column_labels.index("Folder")
|
folders_label_col = column_labels.index("Folder")
|
||||||
|
version_col = column_labels.index("Version")
|
||||||
|
status_col = column_labels.index("Status")
|
||||||
|
published_time_col = column_labels.index("Time")
|
||||||
|
author_col = column_labels.index("Author")
|
||||||
|
frame_range_col = column_labels.index("Frames")
|
||||||
|
duration_col = column_labels.index("Duration")
|
||||||
|
handles_col = column_labels.index("Handles")
|
||||||
|
step_col = column_labels.index("Step")
|
||||||
in_scene_col = column_labels.index("In scene")
|
in_scene_col = column_labels.index("In scene")
|
||||||
sitesync_avail_col = column_labels.index("Availability")
|
sitesync_avail_col = column_labels.index("Availability")
|
||||||
|
_display_role_mapping = {
|
||||||
|
product_name_col: QtCore.Qt.DisplayRole,
|
||||||
|
product_type_col: PRODUCT_TYPE_ROLE,
|
||||||
|
folders_label_col: FOLDER_LABEL_ROLE,
|
||||||
|
version_col: VERSION_NAME_ROLE,
|
||||||
|
status_col: VERSION_STATUS_NAME_ROLE,
|
||||||
|
published_time_col: VERSION_PUBLISH_TIME_ROLE,
|
||||||
|
author_col: VERSION_AUTHOR_ROLE,
|
||||||
|
frame_range_col: VERSION_FRAME_RANGE_ROLE,
|
||||||
|
duration_col: VERSION_DURATION_ROLE,
|
||||||
|
handles_col: VERSION_HANDLES_ROLE,
|
||||||
|
step_col: VERSION_STEP_ROLE,
|
||||||
|
in_scene_col: PRODUCT_IN_SCENE_ROLE,
|
||||||
|
sitesync_avail_col: VERSION_AVAILABLE_ROLE,
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, controller):
|
def __init__(self, controller):
|
||||||
super(ProductsModel, self).__init__()
|
super(ProductsModel, self).__init__()
|
||||||
|
|
@ -96,6 +125,7 @@ class ProductsModel(QtGui.QStandardItemModel):
|
||||||
|
|
||||||
self._last_project_name = None
|
self._last_project_name = None
|
||||||
self._last_folder_ids = []
|
self._last_folder_ids = []
|
||||||
|
self._last_project_statuses = {}
|
||||||
|
|
||||||
def get_product_item_indexes(self):
|
def get_product_item_indexes(self):
|
||||||
return [
|
return [
|
||||||
|
|
@ -141,6 +171,15 @@ class ProductsModel(QtGui.QStandardItemModel):
|
||||||
if not index.isValid():
|
if not index.isValid():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
if role in (VERSION_STATUS_SHORT_ROLE, VERSION_STATUS_COLOR_ROLE):
|
||||||
|
status_name = self.data(index, VERSION_STATUS_NAME_ROLE)
|
||||||
|
status_item = self._last_project_statuses.get(status_name)
|
||||||
|
if status_item is None:
|
||||||
|
return ""
|
||||||
|
if role == VERSION_STATUS_SHORT_ROLE:
|
||||||
|
return status_item.short
|
||||||
|
return status_item.color
|
||||||
|
|
||||||
col = index.column()
|
col = index.column()
|
||||||
if col == 0:
|
if col == 0:
|
||||||
return super(ProductsModel, self).data(index, role)
|
return super(ProductsModel, self).data(index, role)
|
||||||
|
|
@ -160,7 +199,9 @@ class ProductsModel(QtGui.QStandardItemModel):
|
||||||
product_item = self._product_items_by_id.get(product_id)
|
product_item = self._product_items_by_id.get(product_id)
|
||||||
if product_item is None:
|
if product_item is None:
|
||||||
return None
|
return None
|
||||||
return list(product_item.version_items.values())
|
product_items = list(product_item.version_items.values())
|
||||||
|
product_items.sort(reverse=True)
|
||||||
|
return product_items
|
||||||
|
|
||||||
if role == QtCore.Qt.EditRole:
|
if role == QtCore.Qt.EditRole:
|
||||||
return None
|
return None
|
||||||
|
|
@ -168,29 +209,8 @@ class ProductsModel(QtGui.QStandardItemModel):
|
||||||
if role == QtCore.Qt.DisplayRole:
|
if role == QtCore.Qt.DisplayRole:
|
||||||
if not index.data(PRODUCT_ID_ROLE):
|
if not index.data(PRODUCT_ID_ROLE):
|
||||||
return None
|
return None
|
||||||
if col == self.version_col:
|
role = self._display_role_mapping.get(col)
|
||||||
role = VERSION_NAME_ROLE
|
if role is None:
|
||||||
elif col == 1:
|
|
||||||
role = PRODUCT_TYPE_ROLE
|
|
||||||
elif col == 2:
|
|
||||||
role = FOLDER_LABEL_ROLE
|
|
||||||
elif col == 4:
|
|
||||||
role = VERSION_PUBLISH_TIME_ROLE
|
|
||||||
elif col == 5:
|
|
||||||
role = VERSION_AUTHOR_ROLE
|
|
||||||
elif col == 6:
|
|
||||||
role = VERSION_FRAME_RANGE_ROLE
|
|
||||||
elif col == 7:
|
|
||||||
role = VERSION_DURATION_ROLE
|
|
||||||
elif col == 8:
|
|
||||||
role = VERSION_HANDLES_ROLE
|
|
||||||
elif col == 9:
|
|
||||||
role = VERSION_STEP_ROLE
|
|
||||||
elif col == 10:
|
|
||||||
role = PRODUCT_IN_SCENE_ROLE
|
|
||||||
elif col == 11:
|
|
||||||
role = VERSION_AVAILABLE_ROLE
|
|
||||||
else:
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
index = self.index(index.row(), 0, index.parent())
|
index = self.index(index.row(), 0, index.parent())
|
||||||
|
|
@ -312,6 +332,7 @@ class ProductsModel(QtGui.QStandardItemModel):
|
||||||
version_item.published_time, VERSION_PUBLISH_TIME_ROLE
|
version_item.published_time, VERSION_PUBLISH_TIME_ROLE
|
||||||
)
|
)
|
||||||
model_item.setData(version_item.author, VERSION_AUTHOR_ROLE)
|
model_item.setData(version_item.author, VERSION_AUTHOR_ROLE)
|
||||||
|
model_item.setData(version_item.status, VERSION_STATUS_NAME_ROLE)
|
||||||
model_item.setData(version_item.frame_range, VERSION_FRAME_RANGE_ROLE)
|
model_item.setData(version_item.frame_range, VERSION_FRAME_RANGE_ROLE)
|
||||||
model_item.setData(version_item.duration, VERSION_DURATION_ROLE)
|
model_item.setData(version_item.duration, VERSION_DURATION_ROLE)
|
||||||
model_item.setData(version_item.handles, VERSION_HANDLES_ROLE)
|
model_item.setData(version_item.handles, VERSION_HANDLES_ROLE)
|
||||||
|
|
@ -393,6 +414,11 @@ class ProductsModel(QtGui.QStandardItemModel):
|
||||||
|
|
||||||
self._last_project_name = project_name
|
self._last_project_name = project_name
|
||||||
self._last_folder_ids = folder_ids
|
self._last_folder_ids = folder_ids
|
||||||
|
status_items = self._controller.get_project_status_items(project_name)
|
||||||
|
self._last_project_statuses = {
|
||||||
|
status_item.name: status_item
|
||||||
|
for status_item in status_items
|
||||||
|
}
|
||||||
|
|
||||||
active_site_icon_def = self._controller.get_active_site_icon_def(
|
active_site_icon_def = self._controller.get_active_site_icon_def(
|
||||||
project_name
|
project_name
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ from ayon_core.tools.utils import (
|
||||||
RecursiveSortFilterProxyModel,
|
RecursiveSortFilterProxyModel,
|
||||||
DeselectableTreeView,
|
DeselectableTreeView,
|
||||||
)
|
)
|
||||||
from ayon_core.tools.utils.delegates import PrettyTimeDelegate
|
from ayon_core.tools.utils.delegates import PrettyTimeDelegate, StatusDelegate
|
||||||
|
|
||||||
from .products_model import (
|
from .products_model import (
|
||||||
ProductsModel,
|
ProductsModel,
|
||||||
|
|
@ -17,12 +17,16 @@ from .products_model import (
|
||||||
FOLDER_ID_ROLE,
|
FOLDER_ID_ROLE,
|
||||||
PRODUCT_ID_ROLE,
|
PRODUCT_ID_ROLE,
|
||||||
VERSION_ID_ROLE,
|
VERSION_ID_ROLE,
|
||||||
|
VERSION_STATUS_NAME_ROLE,
|
||||||
|
VERSION_STATUS_SHORT_ROLE,
|
||||||
|
VERSION_STATUS_COLOR_ROLE,
|
||||||
|
VERSION_STATUS_ICON_ROLE,
|
||||||
VERSION_THUMBNAIL_ID_ROLE,
|
VERSION_THUMBNAIL_ID_ROLE,
|
||||||
)
|
)
|
||||||
from .products_delegates import (
|
from .products_delegates import (
|
||||||
VersionDelegate,
|
VersionDelegate,
|
||||||
LoadedInSceneDelegate,
|
LoadedInSceneDelegate,
|
||||||
SiteSyncDelegate
|
SiteSyncDelegate,
|
||||||
)
|
)
|
||||||
from .actions_utils import show_actions_menu
|
from .actions_utils import show_actions_menu
|
||||||
|
|
||||||
|
|
@ -89,6 +93,7 @@ class ProductsWidget(QtWidgets.QWidget):
|
||||||
90, # Product type
|
90, # Product type
|
||||||
130, # Folder label
|
130, # Folder label
|
||||||
60, # Version
|
60, # Version
|
||||||
|
100, # Status
|
||||||
125, # Time
|
125, # Time
|
||||||
75, # Author
|
75, # Author
|
||||||
75, # Frames
|
75, # Frames
|
||||||
|
|
@ -128,20 +133,24 @@ class ProductsWidget(QtWidgets.QWidget):
|
||||||
products_view.setColumnWidth(idx, width)
|
products_view.setColumnWidth(idx, width)
|
||||||
|
|
||||||
version_delegate = VersionDelegate()
|
version_delegate = VersionDelegate()
|
||||||
products_view.setItemDelegateForColumn(
|
|
||||||
products_model.version_col, version_delegate)
|
|
||||||
|
|
||||||
time_delegate = PrettyTimeDelegate()
|
time_delegate = PrettyTimeDelegate()
|
||||||
products_view.setItemDelegateForColumn(
|
status_delegate = StatusDelegate(
|
||||||
products_model.published_time_col, time_delegate)
|
VERSION_STATUS_NAME_ROLE,
|
||||||
|
VERSION_STATUS_SHORT_ROLE,
|
||||||
|
VERSION_STATUS_COLOR_ROLE,
|
||||||
|
VERSION_STATUS_ICON_ROLE,
|
||||||
|
)
|
||||||
in_scene_delegate = LoadedInSceneDelegate()
|
in_scene_delegate = LoadedInSceneDelegate()
|
||||||
products_view.setItemDelegateForColumn(
|
|
||||||
products_model.in_scene_col, in_scene_delegate)
|
|
||||||
|
|
||||||
sitesync_delegate = SiteSyncDelegate()
|
sitesync_delegate = SiteSyncDelegate()
|
||||||
products_view.setItemDelegateForColumn(
|
|
||||||
products_model.sitesync_avail_col, sitesync_delegate)
|
for col, delegate in (
|
||||||
|
(products_model.version_col, version_delegate),
|
||||||
|
(products_model.published_time_col, time_delegate),
|
||||||
|
(products_model.status_col, status_delegate),
|
||||||
|
(products_model.in_scene_col, in_scene_delegate),
|
||||||
|
(products_model.sitesync_avail_col, sitesync_delegate),
|
||||||
|
):
|
||||||
|
products_view.setItemDelegateForColumn(col, delegate)
|
||||||
|
|
||||||
main_layout = QtWidgets.QHBoxLayout(self)
|
main_layout = QtWidgets.QHBoxLayout(self)
|
||||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
@ -175,6 +184,7 @@ class ProductsWidget(QtWidgets.QWidget):
|
||||||
|
|
||||||
self._version_delegate = version_delegate
|
self._version_delegate = version_delegate
|
||||||
self._time_delegate = time_delegate
|
self._time_delegate = time_delegate
|
||||||
|
self._status_delegate = status_delegate
|
||||||
self._in_scene_delegate = in_scene_delegate
|
self._in_scene_delegate = in_scene_delegate
|
||||||
self._sitesync_delegate = sitesync_delegate
|
self._sitesync_delegate = sitesync_delegate
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -335,9 +335,7 @@ class LoaderWindow(QtWidgets.QWidget):
|
||||||
|
|
||||||
def closeEvent(self, event):
|
def closeEvent(self, event):
|
||||||
super(LoaderWindow, self).closeEvent(event)
|
super(LoaderWindow, self).closeEvent(event)
|
||||||
# Deselect project so current context will be selected
|
|
||||||
# on next 'showEvent'
|
|
||||||
self._controller.set_selected_project(None)
|
|
||||||
self._reset_on_show = True
|
self._reset_on_show = True
|
||||||
|
|
||||||
def keyPressEvent(self, event):
|
def keyPressEvent(self, event):
|
||||||
|
|
|
||||||
|
|
@ -723,7 +723,6 @@ class ProjectPushItemProcess:
|
||||||
dst_project_name = self._item.dst_project_name
|
dst_project_name = self._item.dst_project_name
|
||||||
dst_folder_id = self._item.dst_folder_id
|
dst_folder_id = self._item.dst_folder_id
|
||||||
dst_task_name = self._item.dst_task_name
|
dst_task_name = self._item.dst_task_name
|
||||||
dst_task_name_low = dst_task_name.lower()
|
|
||||||
new_folder_name = self._item.new_folder_name
|
new_folder_name = self._item.new_folder_name
|
||||||
if not dst_folder_id and not new_folder_name:
|
if not dst_folder_id and not new_folder_name:
|
||||||
self._status.set_failed(
|
self._status.set_failed(
|
||||||
|
|
@ -765,7 +764,7 @@ class ProjectPushItemProcess:
|
||||||
dst_project_name, folder_ids=[folder_entity["id"]]
|
dst_project_name, folder_ids=[folder_entity["id"]]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
task_info = folder_tasks.get(dst_task_name_low)
|
task_info = folder_tasks.get(dst_task_name.lower())
|
||||||
if not task_info:
|
if not task_info:
|
||||||
self._status.set_failed(
|
self._status.set_failed(
|
||||||
f"Could find task with name \"{dst_task_name}\""
|
f"Could find task with name \"{dst_task_name}\""
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
import ayon_api
|
import ayon_api
|
||||||
|
|
||||||
from ayon_core.lib.events import QueuedEventSystem
|
from ayon_core.lib.events import QueuedEventSystem
|
||||||
from ayon_core.host import ILoadHost
|
from ayon_core.host import HostBase
|
||||||
from ayon_core.pipeline import (
|
from ayon_core.pipeline import (
|
||||||
registered_host,
|
registered_host,
|
||||||
get_current_context,
|
get_current_context,
|
||||||
)
|
)
|
||||||
from ayon_core.tools.common_models import HierarchyModel
|
from ayon_core.tools.common_models import HierarchyModel, ProjectsModel
|
||||||
|
|
||||||
from .models import SiteSyncModel
|
from .models import SiteSyncModel, ContainersModel
|
||||||
|
|
||||||
|
|
||||||
class SceneInventoryController:
|
class SceneInventoryController:
|
||||||
|
|
@ -28,11 +28,16 @@ class SceneInventoryController:
|
||||||
self._current_folder_id = None
|
self._current_folder_id = None
|
||||||
self._current_folder_set = False
|
self._current_folder_set = False
|
||||||
|
|
||||||
|
self._containers_model = ContainersModel(self)
|
||||||
self._sitesync_model = SiteSyncModel(self)
|
self._sitesync_model = SiteSyncModel(self)
|
||||||
# Switch dialog requirements
|
# Switch dialog requirements
|
||||||
self._hierarchy_model = HierarchyModel(self)
|
self._hierarchy_model = HierarchyModel(self)
|
||||||
|
self._projects_model = ProjectsModel(self)
|
||||||
self._event_system = self._create_event_system()
|
self._event_system = self._create_event_system()
|
||||||
|
|
||||||
|
def get_host(self) -> HostBase:
|
||||||
|
return self._host
|
||||||
|
|
||||||
def emit_event(self, topic, data=None, source=None):
|
def emit_event(self, topic, data=None, source=None):
|
||||||
if data is None:
|
if data is None:
|
||||||
data = {}
|
data = {}
|
||||||
|
|
@ -47,6 +52,7 @@ class SceneInventoryController:
|
||||||
self._current_folder_id = None
|
self._current_folder_id = None
|
||||||
self._current_folder_set = False
|
self._current_folder_set = False
|
||||||
|
|
||||||
|
self._containers_model.reset()
|
||||||
self._sitesync_model.reset()
|
self._sitesync_model.reset()
|
||||||
self._hierarchy_model.reset()
|
self._hierarchy_model.reset()
|
||||||
|
|
||||||
|
|
@ -80,13 +86,32 @@ class SceneInventoryController:
|
||||||
self._current_folder_set = True
|
self._current_folder_set = True
|
||||||
return self._current_folder_id
|
return self._current_folder_id
|
||||||
|
|
||||||
|
def get_project_status_items(self):
|
||||||
|
project_name = self.get_current_project_name()
|
||||||
|
return self._projects_model.get_project_status_items(
|
||||||
|
project_name, None
|
||||||
|
)
|
||||||
|
|
||||||
|
# Containers methods
|
||||||
def get_containers(self):
|
def get_containers(self):
|
||||||
host = self._host
|
return self._containers_model.get_containers()
|
||||||
if isinstance(host, ILoadHost):
|
|
||||||
return list(host.get_containers())
|
def get_containers_by_item_ids(self, item_ids):
|
||||||
elif hasattr(host, "ls"):
|
return self._containers_model.get_containers_by_item_ids(item_ids)
|
||||||
return list(host.ls())
|
|
||||||
return []
|
def get_container_items(self):
|
||||||
|
return self._containers_model.get_container_items()
|
||||||
|
|
||||||
|
def get_container_items_by_id(self, item_ids):
|
||||||
|
return self._containers_model.get_container_items_by_id(item_ids)
|
||||||
|
|
||||||
|
def get_representation_info_items(self, representation_ids):
|
||||||
|
return self._containers_model.get_representation_info_items(
|
||||||
|
representation_ids
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_version_items(self, product_ids):
|
||||||
|
return self._containers_model.get_version_items(product_ids)
|
||||||
|
|
||||||
# Site Sync methods
|
# Site Sync methods
|
||||||
def is_sitesync_enabled(self):
|
def is_sitesync_enabled(self):
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,10 @@
|
||||||
import numbers
|
|
||||||
|
|
||||||
import ayon_api
|
|
||||||
|
|
||||||
from ayon_core.pipeline import HeroVersionType
|
|
||||||
from ayon_core.tools.utils.models import TreeModel
|
|
||||||
from ayon_core.tools.utils.lib import format_version
|
|
||||||
|
|
||||||
from qtpy import QtWidgets, QtCore, QtGui
|
from qtpy import QtWidgets, QtCore, QtGui
|
||||||
|
|
||||||
|
from .model import VERSION_LABEL_ROLE
|
||||||
|
|
||||||
|
|
||||||
class VersionDelegate(QtWidgets.QStyledItemDelegate):
|
class VersionDelegate(QtWidgets.QStyledItemDelegate):
|
||||||
"""A delegate that display version integer formatted as version string."""
|
"""A delegate that display version integer formatted as version string."""
|
||||||
|
|
||||||
version_changed = QtCore.Signal()
|
|
||||||
first_run = False
|
|
||||||
lock = False
|
|
||||||
|
|
||||||
def __init__(self, controller, *args, **kwargs):
|
|
||||||
self._controller = controller
|
|
||||||
super(VersionDelegate, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def get_project_name(self):
|
|
||||||
return self._controller.get_current_project_name()
|
|
||||||
|
|
||||||
def displayText(self, value, locale):
|
|
||||||
if isinstance(value, HeroVersionType):
|
|
||||||
return format_version(value)
|
|
||||||
if not isinstance(value, numbers.Integral):
|
|
||||||
# For cases where no version is resolved like NOT FOUND cases
|
|
||||||
# where a representation might not exist in current database
|
|
||||||
return
|
|
||||||
|
|
||||||
return format_version(value)
|
|
||||||
|
|
||||||
def paint(self, painter, option, index):
|
def paint(self, painter, option, index):
|
||||||
fg_color = index.data(QtCore.Qt.ForegroundRole)
|
fg_color = index.data(QtCore.Qt.ForegroundRole)
|
||||||
if fg_color:
|
if fg_color:
|
||||||
|
|
@ -44,7 +16,7 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate):
|
||||||
fg_color = None
|
fg_color = None
|
||||||
|
|
||||||
if not fg_color:
|
if not fg_color:
|
||||||
return super(VersionDelegate, self).paint(painter, option, index)
|
return super().paint(painter, option, index)
|
||||||
|
|
||||||
if option.widget:
|
if option.widget:
|
||||||
style = option.widget.style()
|
style = option.widget.style()
|
||||||
|
|
@ -60,9 +32,7 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate):
|
||||||
|
|
||||||
painter.save()
|
painter.save()
|
||||||
|
|
||||||
text = self.displayText(
|
text = index.data(VERSION_LABEL_ROLE)
|
||||||
index.data(QtCore.Qt.DisplayRole), option.locale
|
|
||||||
)
|
|
||||||
pen = painter.pen()
|
pen = painter.pen()
|
||||||
pen.setColor(fg_color)
|
pen.setColor(fg_color)
|
||||||
painter.setPen(pen)
|
painter.setPen(pen)
|
||||||
|
|
@ -82,77 +52,3 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate):
|
||||||
)
|
)
|
||||||
|
|
||||||
painter.restore()
|
painter.restore()
|
||||||
|
|
||||||
def createEditor(self, parent, option, index):
|
|
||||||
item = index.data(TreeModel.ItemRole)
|
|
||||||
if item.get("isGroup") or item.get("isMerged"):
|
|
||||||
return
|
|
||||||
|
|
||||||
editor = QtWidgets.QComboBox(parent)
|
|
||||||
|
|
||||||
def commit_data():
|
|
||||||
if not self.first_run:
|
|
||||||
self.commitData.emit(editor) # Update model data
|
|
||||||
self.version_changed.emit() # Display model data
|
|
||||||
editor.currentIndexChanged.connect(commit_data)
|
|
||||||
|
|
||||||
self.first_run = True
|
|
||||||
self.lock = False
|
|
||||||
|
|
||||||
return editor
|
|
||||||
|
|
||||||
def setEditorData(self, editor, index):
|
|
||||||
if self.lock:
|
|
||||||
# Only set editor data once per delegation
|
|
||||||
return
|
|
||||||
|
|
||||||
editor.clear()
|
|
||||||
|
|
||||||
# Current value of the index
|
|
||||||
item = index.data(TreeModel.ItemRole)
|
|
||||||
value = index.data(QtCore.Qt.DisplayRole)
|
|
||||||
|
|
||||||
project_name = self.get_project_name()
|
|
||||||
# Add all available versions to the editor
|
|
||||||
product_id = item["version_entity"]["productId"]
|
|
||||||
version_entities = list(sorted(
|
|
||||||
ayon_api.get_versions(
|
|
||||||
project_name, product_ids={product_id}, active=True
|
|
||||||
),
|
|
||||||
key=lambda item: abs(item["version"])
|
|
||||||
))
|
|
||||||
|
|
||||||
selected = None
|
|
||||||
items = []
|
|
||||||
is_hero_version = value < 0
|
|
||||||
for version_entity in version_entities:
|
|
||||||
version = version_entity["version"]
|
|
||||||
label = format_version(version)
|
|
||||||
item = QtGui.QStandardItem(label)
|
|
||||||
item.setData(version_entity, QtCore.Qt.UserRole)
|
|
||||||
items.append(item)
|
|
||||||
|
|
||||||
if (
|
|
||||||
version == value
|
|
||||||
or is_hero_version and version < 0
|
|
||||||
):
|
|
||||||
selected = item
|
|
||||||
|
|
||||||
# Reverse items so latest versions be upper
|
|
||||||
items.reverse()
|
|
||||||
for item in items:
|
|
||||||
editor.model().appendRow(item)
|
|
||||||
|
|
||||||
index = 0
|
|
||||||
if selected:
|
|
||||||
index = selected.row()
|
|
||||||
|
|
||||||
# Will trigger index-change signal
|
|
||||||
editor.setCurrentIndex(index)
|
|
||||||
self.first_run = False
|
|
||||||
self.lock = True
|
|
||||||
|
|
||||||
def setModelData(self, editor, model, index):
|
|
||||||
"""Apply the integer version back in the model"""
|
|
||||||
version = editor.itemData(editor.currentIndex())
|
|
||||||
model.setData(index, version["name"])
|
|
||||||
|
|
|
||||||
|
|
@ -1,57 +1,113 @@
|
||||||
import re
|
import re
|
||||||
import logging
|
import logging
|
||||||
import uuid
|
|
||||||
|
|
||||||
from collections import defaultdict
|
import collections
|
||||||
|
|
||||||
import ayon_api
|
|
||||||
from qtpy import QtCore, QtGui
|
from qtpy import QtCore, QtGui
|
||||||
import qtawesome
|
import qtawesome
|
||||||
|
|
||||||
from ayon_core.pipeline import (
|
|
||||||
get_current_project_name,
|
|
||||||
HeroVersionType,
|
|
||||||
)
|
|
||||||
from ayon_core.style import get_default_entity_icon_color
|
from ayon_core.style import get_default_entity_icon_color
|
||||||
from ayon_core.tools.utils import get_qt_icon
|
from ayon_core.tools.utils import get_qt_icon
|
||||||
from ayon_core.tools.utils.models import TreeModel, Item
|
from ayon_core.tools.utils.lib import format_version
|
||||||
|
|
||||||
|
ITEM_ID_ROLE = QtCore.Qt.UserRole + 1
|
||||||
|
NAME_COLOR_ROLE = QtCore.Qt.UserRole + 2
|
||||||
|
COUNT_ROLE = QtCore.Qt.UserRole + 3
|
||||||
|
IS_CONTAINER_ITEM_ROLE = QtCore.Qt.UserRole + 4
|
||||||
|
VERSION_IS_LATEST_ROLE = QtCore.Qt.UserRole + 5
|
||||||
|
VERSION_IS_HERO_ROLE = QtCore.Qt.UserRole + 6
|
||||||
|
VERSION_LABEL_ROLE = QtCore.Qt.UserRole + 7
|
||||||
|
VERSION_COLOR_ROLE = QtCore.Qt.UserRole + 8
|
||||||
|
STATUS_NAME_ROLE = QtCore.Qt.UserRole + 9
|
||||||
|
STATUS_COLOR_ROLE = QtCore.Qt.UserRole + 10
|
||||||
|
STATUS_SHORT_ROLE = QtCore.Qt.UserRole + 11
|
||||||
|
STATUS_ICON_ROLE = QtCore.Qt.UserRole + 12
|
||||||
|
PRODUCT_ID_ROLE = QtCore.Qt.UserRole + 13
|
||||||
|
PRODUCT_TYPE_ROLE = QtCore.Qt.UserRole + 14
|
||||||
|
PRODUCT_TYPE_ICON_ROLE = QtCore.Qt.UserRole + 15
|
||||||
|
PRODUCT_GROUP_NAME_ROLE = QtCore.Qt.UserRole + 16
|
||||||
|
PRODUCT_GROUP_ICON_ROLE = QtCore.Qt.UserRole + 17
|
||||||
|
LOADER_NAME_ROLE = QtCore.Qt.UserRole + 18
|
||||||
|
OBJECT_NAME_ROLE = QtCore.Qt.UserRole + 19
|
||||||
|
ACTIVE_SITE_PROGRESS_ROLE = QtCore.Qt.UserRole + 20
|
||||||
|
REMOTE_SITE_PROGRESS_ROLE = QtCore.Qt.UserRole + 21
|
||||||
|
ACTIVE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 22
|
||||||
|
REMOTE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 23
|
||||||
|
# This value hold unique value of container that should be used to identify
|
||||||
|
# containers inbetween refresh.
|
||||||
|
ITEM_UNIQUE_NAME_ROLE = QtCore.Qt.UserRole + 24
|
||||||
|
|
||||||
|
|
||||||
def walk_hierarchy(node):
|
class InventoryModel(QtGui.QStandardItemModel):
|
||||||
"""Recursively yield group node."""
|
|
||||||
for child in node.children():
|
|
||||||
if child.get("isGroupNode"):
|
|
||||||
yield child
|
|
||||||
|
|
||||||
for _child in walk_hierarchy(child):
|
|
||||||
yield _child
|
|
||||||
|
|
||||||
|
|
||||||
class InventoryModel(TreeModel):
|
|
||||||
"""The model for the inventory"""
|
"""The model for the inventory"""
|
||||||
|
|
||||||
Columns = [
|
column_labels = [
|
||||||
"Name",
|
"Name",
|
||||||
"version",
|
"Version",
|
||||||
"count",
|
"Status",
|
||||||
"productType",
|
"Count",
|
||||||
"group",
|
"Product type",
|
||||||
"loader",
|
"Group",
|
||||||
"objectName",
|
"Loader",
|
||||||
"active_site",
|
"Object name",
|
||||||
"remote_site",
|
"Active site",
|
||||||
|
"Remote site",
|
||||||
]
|
]
|
||||||
active_site_col = Columns.index("active_site")
|
name_col = column_labels.index("Name")
|
||||||
remote_site_col = Columns.index("remote_site")
|
version_col = column_labels.index("Version")
|
||||||
|
status_col = column_labels.index("Status")
|
||||||
|
count_col = column_labels.index("Count")
|
||||||
|
product_type_col = column_labels.index("Product type")
|
||||||
|
product_group_col = column_labels.index("Group")
|
||||||
|
loader_col = column_labels.index("Loader")
|
||||||
|
object_name_col = column_labels.index("Object name")
|
||||||
|
active_site_col = column_labels.index("Active site")
|
||||||
|
remote_site_col = column_labels.index("Remote site")
|
||||||
|
display_role_by_column = {
|
||||||
|
name_col: QtCore.Qt.DisplayRole,
|
||||||
|
version_col: VERSION_LABEL_ROLE,
|
||||||
|
status_col: STATUS_NAME_ROLE,
|
||||||
|
count_col: COUNT_ROLE,
|
||||||
|
product_type_col: PRODUCT_TYPE_ROLE,
|
||||||
|
product_group_col: PRODUCT_GROUP_NAME_ROLE,
|
||||||
|
loader_col: LOADER_NAME_ROLE,
|
||||||
|
object_name_col: OBJECT_NAME_ROLE,
|
||||||
|
active_site_col: ACTIVE_SITE_PROGRESS_ROLE,
|
||||||
|
remote_site_col: REMOTE_SITE_PROGRESS_ROLE,
|
||||||
|
}
|
||||||
|
decoration_role_by_column = {
|
||||||
|
name_col: QtCore.Qt.DecorationRole,
|
||||||
|
product_type_col: PRODUCT_TYPE_ICON_ROLE,
|
||||||
|
product_group_col: PRODUCT_GROUP_ICON_ROLE,
|
||||||
|
active_site_col: ACTIVE_SITE_ICON_ROLE,
|
||||||
|
remote_site_col: REMOTE_SITE_ICON_ROLE,
|
||||||
|
}
|
||||||
|
foreground_role_by_column = {
|
||||||
|
name_col: NAME_COLOR_ROLE,
|
||||||
|
version_col: VERSION_COLOR_ROLE,
|
||||||
|
status_col: STATUS_COLOR_ROLE
|
||||||
|
}
|
||||||
|
width_by_column = {
|
||||||
|
name_col: 250,
|
||||||
|
version_col: 55,
|
||||||
|
status_col: 100,
|
||||||
|
count_col: 55,
|
||||||
|
product_type_col: 150,
|
||||||
|
product_group_col: 120,
|
||||||
|
loader_col: 150,
|
||||||
|
}
|
||||||
|
|
||||||
OUTDATED_COLOR = QtGui.QColor(235, 30, 30)
|
OUTDATED_COLOR = QtGui.QColor(235, 30, 30)
|
||||||
CHILD_OUTDATED_COLOR = QtGui.QColor(200, 160, 30)
|
CHILD_OUTDATED_COLOR = QtGui.QColor(200, 160, 30)
|
||||||
GRAYOUT_COLOR = QtGui.QColor(160, 160, 160)
|
GRAYOUT_COLOR = QtGui.QColor(160, 160, 160)
|
||||||
|
|
||||||
UniqueRole = QtCore.Qt.UserRole + 2 # unique label role
|
|
||||||
|
|
||||||
def __init__(self, controller, parent=None):
|
def __init__(self, controller, parent=None):
|
||||||
super(InventoryModel, self).__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
|
self.setColumnCount(len(self.column_labels))
|
||||||
|
for idx, label in enumerate(self.column_labels):
|
||||||
|
self.setHeaderData(idx, QtCore.Qt.Horizontal, label)
|
||||||
|
|
||||||
self.log = logging.getLogger(self.__class__.__name__)
|
self.log = logging.getLogger(self.__class__.__name__)
|
||||||
|
|
||||||
self._controller = controller
|
self._controller = controller
|
||||||
|
|
@ -60,103 +116,217 @@ class InventoryModel(TreeModel):
|
||||||
|
|
||||||
self._default_icon_color = get_default_entity_icon_color()
|
self._default_icon_color = get_default_entity_icon_color()
|
||||||
|
|
||||||
site_icons = self._controller.get_site_provider_icons()
|
|
||||||
|
|
||||||
self._site_icons = {
|
|
||||||
provider: get_qt_icon(icon_def)
|
|
||||||
for provider, icon_def in site_icons.items()
|
|
||||||
}
|
|
||||||
|
|
||||||
def outdated(self, item):
|
def outdated(self, item):
|
||||||
return item.get("isOutdated", True)
|
return item.get("isOutdated", True)
|
||||||
|
|
||||||
|
def refresh(self, selected=None):
|
||||||
|
"""Refresh the model"""
|
||||||
|
# for debugging or testing, injecting items from outside
|
||||||
|
container_items = self._controller.get_container_items()
|
||||||
|
|
||||||
|
self._clear_items()
|
||||||
|
|
||||||
|
items_by_repre_id = {}
|
||||||
|
for container_item in container_items:
|
||||||
|
# if (
|
||||||
|
# selected is not None
|
||||||
|
# and container_item.item_id not in selected
|
||||||
|
# ):
|
||||||
|
# continue
|
||||||
|
repre_id = container_item.representation_id
|
||||||
|
items = items_by_repre_id.setdefault(repre_id, [])
|
||||||
|
items.append(container_item)
|
||||||
|
|
||||||
|
repre_id = set(items_by_repre_id.keys())
|
||||||
|
repre_info_by_id = self._controller.get_representation_info_items(
|
||||||
|
repre_id
|
||||||
|
)
|
||||||
|
product_ids = {
|
||||||
|
repre_info.product_id
|
||||||
|
for repre_info in repre_info_by_id.values()
|
||||||
|
}
|
||||||
|
version_items_by_product_id = self._controller.get_version_items(
|
||||||
|
product_ids
|
||||||
|
)
|
||||||
|
# SiteSync addon information
|
||||||
|
progress_by_id = self._controller.get_representations_site_progress(
|
||||||
|
repre_id
|
||||||
|
)
|
||||||
|
sites_info = self._controller.get_sites_information()
|
||||||
|
site_icons = {
|
||||||
|
provider: get_qt_icon(icon_def)
|
||||||
|
for provider, icon_def in (
|
||||||
|
self._controller.get_site_provider_icons().items()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
status_items_by_name = {
|
||||||
|
status_item.name: status_item
|
||||||
|
for status_item in self._controller.get_project_status_items()
|
||||||
|
}
|
||||||
|
|
||||||
|
group_item_icon = qtawesome.icon(
|
||||||
|
"fa.folder", color=self._default_icon_color
|
||||||
|
)
|
||||||
|
valid_item_icon = qtawesome.icon(
|
||||||
|
"fa.file-o", color=self._default_icon_color
|
||||||
|
)
|
||||||
|
invalid_item_icon = qtawesome.icon(
|
||||||
|
"fa.exclamation-circle", color=self._default_icon_color
|
||||||
|
)
|
||||||
|
group_icon = qtawesome.icon(
|
||||||
|
"fa.object-group", color=self._default_icon_color
|
||||||
|
)
|
||||||
|
product_type_icon = qtawesome.icon(
|
||||||
|
"fa.folder", color="#0091B2"
|
||||||
|
)
|
||||||
|
group_item_font = QtGui.QFont()
|
||||||
|
group_item_font.setBold(True)
|
||||||
|
|
||||||
|
active_site_icon = site_icons.get(sites_info["active_site_provider"])
|
||||||
|
remote_site_icon = site_icons.get(sites_info["remote_site_provider"])
|
||||||
|
|
||||||
|
root_item = self.invisibleRootItem()
|
||||||
|
|
||||||
|
group_items = []
|
||||||
|
for repre_id, container_items in items_by_repre_id.items():
|
||||||
|
repre_info = repre_info_by_id[repre_id]
|
||||||
|
version_label = "N/A"
|
||||||
|
version_color = None
|
||||||
|
is_latest = False
|
||||||
|
is_hero = False
|
||||||
|
status_name = None
|
||||||
|
status_color = None
|
||||||
|
status_short = None
|
||||||
|
if not repre_info.is_valid:
|
||||||
|
group_name = "< Entity N/A >"
|
||||||
|
item_icon = invalid_item_icon
|
||||||
|
|
||||||
|
else:
|
||||||
|
group_name = "{}_{}: ({})".format(
|
||||||
|
repre_info.folder_path.rsplit("/")[-1],
|
||||||
|
repre_info.product_name,
|
||||||
|
repre_info.representation_name
|
||||||
|
)
|
||||||
|
item_icon = valid_item_icon
|
||||||
|
|
||||||
|
version_items = (
|
||||||
|
version_items_by_product_id[repre_info.product_id]
|
||||||
|
)
|
||||||
|
version_item = version_items[repre_info.version_id]
|
||||||
|
version_label = format_version(version_item.version)
|
||||||
|
is_hero = version_item.version < 0
|
||||||
|
is_latest = version_item.is_latest
|
||||||
|
if not is_latest:
|
||||||
|
version_color = self.OUTDATED_COLOR
|
||||||
|
status_name = version_item.status
|
||||||
|
status_item = status_items_by_name.get(status_name)
|
||||||
|
if status_item:
|
||||||
|
status_short = status_item.short
|
||||||
|
status_color = status_item.color
|
||||||
|
|
||||||
|
container_model_items = []
|
||||||
|
for container_item in container_items:
|
||||||
|
unique_name = (
|
||||||
|
repre_info.representation_name
|
||||||
|
+ container_item.object_name or "<none>"
|
||||||
|
)
|
||||||
|
|
||||||
|
item = QtGui.QStandardItem()
|
||||||
|
item.setColumnCount(root_item.columnCount())
|
||||||
|
item.setData(container_item.namespace, QtCore.Qt.DisplayRole)
|
||||||
|
item.setData(self.GRAYOUT_COLOR, NAME_COLOR_ROLE)
|
||||||
|
item.setData(self.GRAYOUT_COLOR, VERSION_COLOR_ROLE)
|
||||||
|
item.setData(item_icon, QtCore.Qt.DecorationRole)
|
||||||
|
item.setData(repre_info.product_id, PRODUCT_ID_ROLE)
|
||||||
|
item.setData(container_item.item_id, ITEM_ID_ROLE)
|
||||||
|
item.setData(version_label, VERSION_LABEL_ROLE)
|
||||||
|
item.setData(container_item.loader_name, LOADER_NAME_ROLE)
|
||||||
|
item.setData(container_item.object_name, OBJECT_NAME_ROLE)
|
||||||
|
item.setData(True, IS_CONTAINER_ITEM_ROLE)
|
||||||
|
item.setData(unique_name, ITEM_UNIQUE_NAME_ROLE)
|
||||||
|
container_model_items.append(item)
|
||||||
|
|
||||||
|
if not container_model_items:
|
||||||
|
continue
|
||||||
|
|
||||||
|
progress = progress_by_id[repre_id]
|
||||||
|
active_site_progress = "{}%".format(
|
||||||
|
max(progress["active_site"], 0) * 100
|
||||||
|
)
|
||||||
|
remote_site_progress = "{}%".format(
|
||||||
|
max(progress["remote_site"], 0) * 100
|
||||||
|
)
|
||||||
|
|
||||||
|
group_item = QtGui.QStandardItem()
|
||||||
|
group_item.setColumnCount(root_item.columnCount())
|
||||||
|
group_item.setData(group_name, QtCore.Qt.DisplayRole)
|
||||||
|
group_item.setData(group_name, ITEM_UNIQUE_NAME_ROLE)
|
||||||
|
group_item.setData(group_item_icon, QtCore.Qt.DecorationRole)
|
||||||
|
group_item.setData(group_item_font, QtCore.Qt.FontRole)
|
||||||
|
group_item.setData(repre_info.product_id, PRODUCT_ID_ROLE)
|
||||||
|
group_item.setData(repre_info.product_type, PRODUCT_TYPE_ROLE)
|
||||||
|
group_item.setData(product_type_icon, PRODUCT_TYPE_ICON_ROLE)
|
||||||
|
group_item.setData(is_latest, VERSION_IS_LATEST_ROLE)
|
||||||
|
group_item.setData(is_hero, VERSION_IS_HERO_ROLE)
|
||||||
|
group_item.setData(version_label, VERSION_LABEL_ROLE)
|
||||||
|
group_item.setData(len(container_items), COUNT_ROLE)
|
||||||
|
group_item.setData(status_name, STATUS_NAME_ROLE)
|
||||||
|
group_item.setData(status_short, STATUS_SHORT_ROLE)
|
||||||
|
group_item.setData(status_color, STATUS_COLOR_ROLE)
|
||||||
|
|
||||||
|
group_item.setData(
|
||||||
|
active_site_progress, ACTIVE_SITE_PROGRESS_ROLE
|
||||||
|
)
|
||||||
|
group_item.setData(
|
||||||
|
remote_site_progress, REMOTE_SITE_PROGRESS_ROLE
|
||||||
|
)
|
||||||
|
group_item.setData(active_site_icon, ACTIVE_SITE_ICON_ROLE)
|
||||||
|
group_item.setData(remote_site_icon, REMOTE_SITE_ICON_ROLE)
|
||||||
|
group_item.setData(False, IS_CONTAINER_ITEM_ROLE)
|
||||||
|
|
||||||
|
if version_color is not None:
|
||||||
|
group_item.setData(version_color, VERSION_COLOR_ROLE)
|
||||||
|
|
||||||
|
if repre_info.product_group:
|
||||||
|
group_item.setData(
|
||||||
|
repre_info.product_group, PRODUCT_GROUP_NAME_ROLE
|
||||||
|
)
|
||||||
|
group_item.setData(group_icon, PRODUCT_GROUP_ICON_ROLE)
|
||||||
|
|
||||||
|
group_item.appendRows(container_model_items)
|
||||||
|
group_items.append(group_item)
|
||||||
|
|
||||||
|
if group_items:
|
||||||
|
root_item.appendRows(group_items)
|
||||||
|
|
||||||
|
def flags(self, index):
|
||||||
|
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
|
||||||
|
|
||||||
def data(self, index, role):
|
def data(self, index, role):
|
||||||
if not index.isValid():
|
if not index.isValid():
|
||||||
return
|
return
|
||||||
|
|
||||||
item = index.internalPointer()
|
col = index.column()
|
||||||
|
if role == QtCore.Qt.DisplayRole:
|
||||||
|
role = self.display_role_by_column.get(col)
|
||||||
|
if role is None:
|
||||||
|
print(col, role)
|
||||||
|
return None
|
||||||
|
|
||||||
if role == QtCore.Qt.FontRole:
|
elif role == QtCore.Qt.DecorationRole:
|
||||||
# Make top-level entries bold
|
role = self.decoration_role_by_column.get(col)
|
||||||
if item.get("isGroupNode") or item.get("isNotSet"): # group-item
|
if role is None:
|
||||||
font = QtGui.QFont()
|
return None
|
||||||
font.setBold(True)
|
|
||||||
return font
|
|
||||||
|
|
||||||
if role == QtCore.Qt.ForegroundRole:
|
elif role == QtCore.Qt.ForegroundRole:
|
||||||
# Set the text color to the OUTDATED_COLOR when the
|
role = self.foreground_role_by_column.get(col)
|
||||||
# collected version is not the same as the highest version
|
if role is None:
|
||||||
key = self.Columns[index.column()]
|
return None
|
||||||
if key == "version": # version
|
|
||||||
if item.get("isGroupNode"): # group-item
|
|
||||||
if self.outdated(item):
|
|
||||||
return self.OUTDATED_COLOR
|
|
||||||
|
|
||||||
if self._hierarchy_view:
|
if col != 0:
|
||||||
# If current group is not outdated, check if any
|
index = self.index(index.row(), 0, index.parent())
|
||||||
# outdated children.
|
|
||||||
for _node in walk_hierarchy(item):
|
|
||||||
if self.outdated(_node):
|
|
||||||
return self.CHILD_OUTDATED_COLOR
|
|
||||||
else:
|
|
||||||
|
|
||||||
if self._hierarchy_view:
|
return super().data(index, role)
|
||||||
# Although this is not a group item, we still need
|
|
||||||
# to distinguish which one contain outdated child.
|
|
||||||
for _node in walk_hierarchy(item):
|
|
||||||
if self.outdated(_node):
|
|
||||||
return self.CHILD_OUTDATED_COLOR.darker(150)
|
|
||||||
|
|
||||||
return self.GRAYOUT_COLOR
|
|
||||||
|
|
||||||
if key == "Name" and not item.get("isGroupNode"):
|
|
||||||
return self.GRAYOUT_COLOR
|
|
||||||
|
|
||||||
# Add icons
|
|
||||||
if role == QtCore.Qt.DecorationRole:
|
|
||||||
if index.column() == 0:
|
|
||||||
# Override color
|
|
||||||
color = item.get("color", self._default_icon_color)
|
|
||||||
if item.get("isGroupNode"): # group-item
|
|
||||||
return qtawesome.icon("fa.folder", color=color)
|
|
||||||
if item.get("isNotSet"):
|
|
||||||
return qtawesome.icon("fa.exclamation-circle", color=color)
|
|
||||||
|
|
||||||
return qtawesome.icon("fa.file-o", color=color)
|
|
||||||
|
|
||||||
if index.column() == 3:
|
|
||||||
# Product type icon
|
|
||||||
return item.get("productTypeIcon", None)
|
|
||||||
|
|
||||||
column_name = self.Columns[index.column()]
|
|
||||||
|
|
||||||
if column_name == "group" and item.get("group"):
|
|
||||||
return qtawesome.icon("fa.object-group",
|
|
||||||
color=get_default_entity_icon_color())
|
|
||||||
|
|
||||||
if item.get("isGroupNode"):
|
|
||||||
if column_name == "active_site":
|
|
||||||
provider = item.get("active_site_provider")
|
|
||||||
return self._site_icons.get(provider)
|
|
||||||
|
|
||||||
if column_name == "remote_site":
|
|
||||||
provider = item.get("remote_site_provider")
|
|
||||||
return self._site_icons.get(provider)
|
|
||||||
|
|
||||||
if role == QtCore.Qt.DisplayRole and item.get("isGroupNode"):
|
|
||||||
column_name = self.Columns[index.column()]
|
|
||||||
progress = None
|
|
||||||
if column_name == "active_site":
|
|
||||||
progress = item.get("active_site_progress", 0)
|
|
||||||
elif column_name == "remote_site":
|
|
||||||
progress = item.get("remote_site_progress", 0)
|
|
||||||
if progress is not None:
|
|
||||||
return "{}%".format(max(progress, 0) * 100)
|
|
||||||
|
|
||||||
if role == self.UniqueRole:
|
|
||||||
return item["representation"] + item.get("objectName", "<none>")
|
|
||||||
|
|
||||||
return super(InventoryModel, self).data(index, role)
|
|
||||||
|
|
||||||
def set_hierarchy_view(self, state):
|
def set_hierarchy_view(self, state):
|
||||||
"""Set whether to display products in hierarchy view."""
|
"""Set whether to display products in hierarchy view."""
|
||||||
|
|
@ -165,299 +335,34 @@ class InventoryModel(TreeModel):
|
||||||
if state != self._hierarchy_view:
|
if state != self._hierarchy_view:
|
||||||
self._hierarchy_view = state
|
self._hierarchy_view = state
|
||||||
|
|
||||||
def refresh(self, selected=None, containers=None):
|
def get_outdated_item_ids(self, ignore_hero=True):
|
||||||
"""Refresh the model"""
|
outdated_item_ids = []
|
||||||
|
root_item = self.invisibleRootItem()
|
||||||
# for debugging or testing, injecting items from outside
|
for row in range(root_item.rowCount()):
|
||||||
if containers is None:
|
group_item = root_item.child(row)
|
||||||
containers = self._controller.get_containers()
|
if group_item.data(VERSION_IS_LATEST_ROLE):
|
||||||
|
|
||||||
self.clear()
|
|
||||||
if not selected or not self._hierarchy_view:
|
|
||||||
self._add_containers(containers)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Filter by cherry-picked items
|
|
||||||
self._add_containers((
|
|
||||||
container
|
|
||||||
for container in containers
|
|
||||||
if container["objectName"] in selected
|
|
||||||
))
|
|
||||||
|
|
||||||
def _add_containers(self, containers, parent=None):
|
|
||||||
"""Add the items to the model.
|
|
||||||
|
|
||||||
The items should be formatted similar to `api.ls()` returns, an item
|
|
||||||
is then represented as:
|
|
||||||
{"filename_v001.ma": [full/filename/of/loaded/filename_v001.ma,
|
|
||||||
full/filename/of/loaded/filename_v001.ma],
|
|
||||||
"nodetype" : "reference",
|
|
||||||
"node": "referenceNode1"}
|
|
||||||
|
|
||||||
Note: When performing an additional call to `add_items` it will *not*
|
|
||||||
group the new items with previously existing item groups of the
|
|
||||||
same type.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
containers (generator): Container items.
|
|
||||||
parent (Item, optional): Set this item as parent for the added
|
|
||||||
items when provided. Defaults to the root of the model.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
node.Item: root node which has children added based on the data
|
|
||||||
"""
|
|
||||||
|
|
||||||
project_name = get_current_project_name()
|
|
||||||
|
|
||||||
self.beginResetModel()
|
|
||||||
|
|
||||||
# Group by representation
|
|
||||||
grouped = defaultdict(lambda: {"containers": list()})
|
|
||||||
for container in containers:
|
|
||||||
repre_id = container["representation"]
|
|
||||||
grouped[repre_id]["containers"].append(container)
|
|
||||||
|
|
||||||
(
|
|
||||||
repres_by_id,
|
|
||||||
versions_by_id,
|
|
||||||
products_by_id,
|
|
||||||
folders_by_id,
|
|
||||||
) = self._query_entities(project_name, set(grouped.keys()))
|
|
||||||
# Add to model
|
|
||||||
not_found = defaultdict(list)
|
|
||||||
not_found_ids = []
|
|
||||||
for repre_id, group_dict in sorted(grouped.items()):
|
|
||||||
group_containers = group_dict["containers"]
|
|
||||||
representation = repres_by_id.get(repre_id)
|
|
||||||
if not representation:
|
|
||||||
not_found["representation"].extend(group_containers)
|
|
||||||
not_found_ids.append(repre_id)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
version_entity = versions_by_id.get(representation["versionId"])
|
if ignore_hero and group_item.data(VERSION_IS_HERO_ROLE):
|
||||||
if not version_entity:
|
|
||||||
not_found["version"].extend(group_containers)
|
|
||||||
not_found_ids.append(repre_id)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
product_entity = products_by_id.get(version_entity["productId"])
|
for idx in range(group_item.rowCount()):
|
||||||
if not product_entity:
|
item = group_item.child(idx)
|
||||||
not_found["product"].extend(group_containers)
|
outdated_item_ids.append(item.data(ITEM_ID_ROLE))
|
||||||
not_found_ids.append(repre_id)
|
return outdated_item_ids
|
||||||
continue
|
|
||||||
|
|
||||||
folder_entity = folders_by_id.get(product_entity["folderId"])
|
def _clear_items(self):
|
||||||
if not folder_entity:
|
root_item = self.invisibleRootItem()
|
||||||
not_found["folder"].extend(group_containers)
|
root_item.removeRows(0, root_item.rowCount())
|
||||||
not_found_ids.append(repre_id)
|
|
||||||
continue
|
|
||||||
|
|
||||||
group_dict.update({
|
|
||||||
"representation": representation,
|
|
||||||
"version": version_entity,
|
|
||||||
"product": product_entity,
|
|
||||||
"folder": folder_entity
|
|
||||||
})
|
|
||||||
|
|
||||||
for _repre_id in not_found_ids:
|
|
||||||
grouped.pop(_repre_id)
|
|
||||||
|
|
||||||
for where, group_containers in not_found.items():
|
|
||||||
# create the group header
|
|
||||||
group_node = Item()
|
|
||||||
name = "< NOT FOUND - {} >".format(where)
|
|
||||||
group_node["Name"] = name
|
|
||||||
group_node["representation"] = name
|
|
||||||
group_node["count"] = len(group_containers)
|
|
||||||
group_node["isGroupNode"] = False
|
|
||||||
group_node["isNotSet"] = True
|
|
||||||
|
|
||||||
self.add_child(group_node, parent=parent)
|
|
||||||
|
|
||||||
for container in group_containers:
|
|
||||||
item_node = Item()
|
|
||||||
item_node.update(container)
|
|
||||||
item_node["Name"] = container.get("objectName", "NO NAME")
|
|
||||||
item_node["isNotFound"] = True
|
|
||||||
self.add_child(item_node, parent=group_node)
|
|
||||||
|
|
||||||
# TODO Use product icons
|
|
||||||
product_type_icon = qtawesome.icon(
|
|
||||||
"fa.folder", color="#0091B2"
|
|
||||||
)
|
|
||||||
# Prepare site sync specific data
|
|
||||||
progress_by_id = self._controller.get_representations_site_progress(
|
|
||||||
set(grouped.keys())
|
|
||||||
)
|
|
||||||
sites_info = self._controller.get_sites_information()
|
|
||||||
|
|
||||||
# Query the highest available version so the model can know
|
|
||||||
# whether current version is currently up-to-date.
|
|
||||||
highest_version_by_product_id = ayon_api.get_last_versions(
|
|
||||||
project_name,
|
|
||||||
product_ids={
|
|
||||||
group["version"]["productId"] for group in grouped.values()
|
|
||||||
},
|
|
||||||
fields={"productId", "version"}
|
|
||||||
)
|
|
||||||
# Map value to `version` key
|
|
||||||
highest_version_by_product_id = {
|
|
||||||
product_id: version["version"]
|
|
||||||
for product_id, version in highest_version_by_product_id.items()
|
|
||||||
}
|
|
||||||
|
|
||||||
for repre_id, group_dict in sorted(grouped.items()):
|
|
||||||
group_containers = group_dict["containers"]
|
|
||||||
repre_entity = group_dict["representation"]
|
|
||||||
version_entity = group_dict["version"]
|
|
||||||
folder_entity = group_dict["folder"]
|
|
||||||
product_entity = group_dict["product"]
|
|
||||||
|
|
||||||
product_type = product_entity["productType"]
|
|
||||||
|
|
||||||
# create the group header
|
|
||||||
group_node = Item()
|
|
||||||
group_node["Name"] = "{}_{}: ({})".format(
|
|
||||||
folder_entity["name"],
|
|
||||||
product_entity["name"],
|
|
||||||
repre_entity["name"]
|
|
||||||
)
|
|
||||||
group_node["representation"] = repre_id
|
|
||||||
|
|
||||||
# Detect hero version type
|
|
||||||
version = version_entity["version"]
|
|
||||||
if version < 0:
|
|
||||||
version = HeroVersionType(version)
|
|
||||||
group_node["version"] = version
|
|
||||||
|
|
||||||
# Check if the version is outdated.
|
|
||||||
# Hero versions are never considered to be outdated.
|
|
||||||
is_outdated = False
|
|
||||||
if not isinstance(version, HeroVersionType):
|
|
||||||
last_version = highest_version_by_product_id.get(
|
|
||||||
version_entity["productId"])
|
|
||||||
if last_version is not None:
|
|
||||||
is_outdated = version_entity["version"] != last_version
|
|
||||||
group_node["isOutdated"] = is_outdated
|
|
||||||
|
|
||||||
group_node["productType"] = product_type or ""
|
|
||||||
group_node["productTypeIcon"] = product_type_icon
|
|
||||||
group_node["count"] = len(group_containers)
|
|
||||||
group_node["isGroupNode"] = True
|
|
||||||
group_node["group"] = product_entity["attrib"].get("productGroup")
|
|
||||||
|
|
||||||
# Site sync specific data
|
|
||||||
progress = progress_by_id[repre_id]
|
|
||||||
group_node.update(sites_info)
|
|
||||||
group_node["active_site_progress"] = progress["active_site"]
|
|
||||||
group_node["remote_site_progress"] = progress["remote_site"]
|
|
||||||
|
|
||||||
self.add_child(group_node, parent=parent)
|
|
||||||
|
|
||||||
for container in group_containers:
|
|
||||||
item_node = Item()
|
|
||||||
item_node.update(container)
|
|
||||||
|
|
||||||
# store the current version on the item
|
|
||||||
item_node["version"] = version_entity["version"]
|
|
||||||
item_node["version_entity"] = version_entity
|
|
||||||
|
|
||||||
# Remapping namespace to item name.
|
|
||||||
# Noted that the name key is capital "N", by doing this, we
|
|
||||||
# can view namespace in GUI without changing container data.
|
|
||||||
item_node["Name"] = container["namespace"]
|
|
||||||
|
|
||||||
self.add_child(item_node, parent=group_node)
|
|
||||||
|
|
||||||
self.endResetModel()
|
|
||||||
|
|
||||||
return self._root_item
|
|
||||||
|
|
||||||
def _query_entities(self, project_name, repre_ids):
|
|
||||||
"""Query entities for representations from containers.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
tuple[dict, dict, dict, dict]: Representation, version, product
|
|
||||||
and folder documents by id.
|
|
||||||
"""
|
|
||||||
|
|
||||||
repres_by_id = {}
|
|
||||||
versions_by_id = {}
|
|
||||||
products_by_id = {}
|
|
||||||
folders_by_id = {}
|
|
||||||
output = (
|
|
||||||
repres_by_id,
|
|
||||||
versions_by_id,
|
|
||||||
products_by_id,
|
|
||||||
folders_by_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
filtered_repre_ids = set()
|
|
||||||
for repre_id in repre_ids:
|
|
||||||
# Filter out invalid representation ids
|
|
||||||
# NOTE: This is added because scenes from OpenPype did contain
|
|
||||||
# ObjectId from mongo.
|
|
||||||
try:
|
|
||||||
uuid.UUID(repre_id)
|
|
||||||
filtered_repre_ids.add(repre_id)
|
|
||||||
except ValueError:
|
|
||||||
continue
|
|
||||||
if not filtered_repre_ids:
|
|
||||||
return output
|
|
||||||
|
|
||||||
repre_entities = ayon_api.get_representations(project_name, repre_ids)
|
|
||||||
repres_by_id.update({
|
|
||||||
repre_entity["id"]: repre_entity
|
|
||||||
for repre_entity in repre_entities
|
|
||||||
})
|
|
||||||
version_ids = {
|
|
||||||
repre_entity["versionId"]
|
|
||||||
for repre_entity in repres_by_id.values()
|
|
||||||
}
|
|
||||||
if not version_ids:
|
|
||||||
return output
|
|
||||||
|
|
||||||
versions_by_id.update({
|
|
||||||
version_entity["id"]: version_entity
|
|
||||||
for version_entity in ayon_api.get_versions(
|
|
||||||
project_name, version_ids=version_ids
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
product_ids = {
|
|
||||||
version_entity["productId"]
|
|
||||||
for version_entity in versions_by_id.values()
|
|
||||||
}
|
|
||||||
if not product_ids:
|
|
||||||
return output
|
|
||||||
|
|
||||||
products_by_id.update({
|
|
||||||
product_entity["id"]: product_entity
|
|
||||||
for product_entity in ayon_api.get_products(
|
|
||||||
project_name, product_ids=product_ids
|
|
||||||
)
|
|
||||||
})
|
|
||||||
folder_ids = {
|
|
||||||
product_entity["folderId"]
|
|
||||||
for product_entity in products_by_id.values()
|
|
||||||
}
|
|
||||||
if not folder_ids:
|
|
||||||
return output
|
|
||||||
|
|
||||||
folders_by_id.update({
|
|
||||||
folder_entity["id"]: folder_entity
|
|
||||||
for folder_entity in ayon_api.get_folders(
|
|
||||||
project_name, folder_ids=folder_ids
|
|
||||||
)
|
|
||||||
})
|
|
||||||
return output
|
|
||||||
|
|
||||||
|
|
||||||
class FilterProxyModel(QtCore.QSortFilterProxyModel):
|
class FilterProxyModel(QtCore.QSortFilterProxyModel):
|
||||||
"""Filter model to where key column's value is in the filtered tags"""
|
"""Filter model to where key column's value is in the filtered tags"""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(FilterProxyModel, self).__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
self.setDynamicSortFilter(True)
|
||||||
|
self.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
|
||||||
self._filter_outdated = False
|
self._filter_outdated = False
|
||||||
self._hierarchy_view = False
|
self._hierarchy_view = False
|
||||||
|
|
||||||
|
|
@ -467,28 +372,23 @@ class FilterProxyModel(QtCore.QSortFilterProxyModel):
|
||||||
|
|
||||||
# Always allow bottom entries (individual containers), since their
|
# Always allow bottom entries (individual containers), since their
|
||||||
# parent group hidden if it wouldn't have been validated.
|
# parent group hidden if it wouldn't have been validated.
|
||||||
rows = model.rowCount(source_index)
|
if source_index.data(IS_CONTAINER_ITEM_ROLE):
|
||||||
if not rows:
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Filter by regex
|
|
||||||
if hasattr(self, "filterRegExp"):
|
|
||||||
regex = self.filterRegExp()
|
|
||||||
else:
|
|
||||||
regex = self.filterRegularExpression()
|
|
||||||
pattern = regex.pattern()
|
|
||||||
if pattern:
|
|
||||||
pattern = re.escape(pattern)
|
|
||||||
|
|
||||||
if not self._matches(row, parent, pattern):
|
|
||||||
return False
|
|
||||||
|
|
||||||
if self._filter_outdated:
|
if self._filter_outdated:
|
||||||
# When filtering to outdated we filter the up to date entries
|
# When filtering to outdated we filter the up to date entries
|
||||||
# thus we "allow" them when they are outdated
|
# thus we "allow" them when they are outdated
|
||||||
if not self._is_outdated(row, parent):
|
if source_index.data(VERSION_IS_LATEST_ROLE):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Filter by regex
|
||||||
|
if hasattr(self, "filterRegularExpression"):
|
||||||
|
regex = self.filterRegularExpression()
|
||||||
|
else:
|
||||||
|
regex = self.filterRegExp()
|
||||||
|
|
||||||
|
if not self._matches(row, parent, regex.pattern()):
|
||||||
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def set_filter_outdated(self, state):
|
def set_filter_outdated(self, state):
|
||||||
|
|
@ -505,37 +405,6 @@ class FilterProxyModel(QtCore.QSortFilterProxyModel):
|
||||||
if state != self._hierarchy_view:
|
if state != self._hierarchy_view:
|
||||||
self._hierarchy_view = state
|
self._hierarchy_view = state
|
||||||
|
|
||||||
def _is_outdated(self, row, parent):
|
|
||||||
"""Return whether row is outdated.
|
|
||||||
|
|
||||||
A row is considered outdated if `isOutdated` data is true or not set.
|
|
||||||
|
|
||||||
"""
|
|
||||||
def outdated(node):
|
|
||||||
return node.get("isOutdated", True)
|
|
||||||
|
|
||||||
index = self.sourceModel().index(row, self.filterKeyColumn(), parent)
|
|
||||||
|
|
||||||
# The scene contents are grouped by "representation", e.g. the same
|
|
||||||
# "representation" loaded twice is grouped under the same header.
|
|
||||||
# Since the version check filters these parent groups we skip that
|
|
||||||
# check for the individual children.
|
|
||||||
has_parent = index.parent().isValid()
|
|
||||||
if has_parent and not self._hierarchy_view:
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Filter to those that have the different version numbers
|
|
||||||
node = index.internalPointer()
|
|
||||||
if outdated(node):
|
|
||||||
return True
|
|
||||||
|
|
||||||
if self._hierarchy_view:
|
|
||||||
for _node in walk_hierarchy(node):
|
|
||||||
if outdated(_node):
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _matches(self, row, parent, pattern):
|
def _matches(self, row, parent, pattern):
|
||||||
"""Return whether row matches regex pattern.
|
"""Return whether row matches regex pattern.
|
||||||
|
|
||||||
|
|
@ -548,38 +417,31 @@ class FilterProxyModel(QtCore.QSortFilterProxyModel):
|
||||||
bool
|
bool
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
if not pattern:
|
||||||
|
return True
|
||||||
|
|
||||||
|
flags = 0
|
||||||
|
if self.sortCaseSensitivity() == QtCore.Qt.CaseInsensitive:
|
||||||
|
flags = re.IGNORECASE
|
||||||
|
|
||||||
|
regex = re.compile(re.escape(pattern), flags=flags)
|
||||||
|
|
||||||
model = self.sourceModel()
|
model = self.sourceModel()
|
||||||
column = self.filterKeyColumn()
|
column = self.filterKeyColumn()
|
||||||
role = self.filterRole()
|
role = self.filterRole()
|
||||||
|
|
||||||
def matches(row, parent, pattern):
|
matches_queue = collections.deque()
|
||||||
|
matches_queue.append((row, parent))
|
||||||
|
while matches_queue:
|
||||||
|
queue_item = matches_queue.popleft()
|
||||||
|
row, parent = queue_item
|
||||||
|
|
||||||
index = model.index(row, column, parent)
|
index = model.index(row, column, parent)
|
||||||
key = model.data(index, role)
|
value = model.data(index, role)
|
||||||
if re.search(pattern, key, re.IGNORECASE):
|
if regex.search(value):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if matches(row, parent, pattern):
|
for idx in range(model.rowCount(index)):
|
||||||
return True
|
matches_queue.append((idx, index))
|
||||||
|
|
||||||
# Also allow if any of the children matches
|
return False
|
||||||
source_index = model.index(row, column, parent)
|
|
||||||
rows = model.rowCount(source_index)
|
|
||||||
|
|
||||||
if any(
|
|
||||||
matches(idx, source_index, pattern)
|
|
||||||
for idx in range(rows)
|
|
||||||
):
|
|
||||||
return True
|
|
||||||
|
|
||||||
if not self._hierarchy_view:
|
|
||||||
return False
|
|
||||||
|
|
||||||
for idx in range(rows):
|
|
||||||
child_index = model.index(idx, column, source_index)
|
|
||||||
child_rows = model.rowCount(child_index)
|
|
||||||
return any(
|
|
||||||
self._matches(child_idx, child_index, pattern)
|
|
||||||
for child_idx in range(child_rows)
|
|
||||||
)
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
|
from .containers import ContainersModel
|
||||||
from .sitesync import SiteSyncModel
|
from .sitesync import SiteSyncModel
|
||||||
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
"ContainersModel",
|
||||||
"SiteSyncModel",
|
"SiteSyncModel",
|
||||||
)
|
)
|
||||||
|
|
|
||||||
343
client/ayon_core/tools/sceneinventory/models/containers.py
Normal file
343
client/ayon_core/tools/sceneinventory/models/containers.py
Normal file
|
|
@ -0,0 +1,343 @@
|
||||||
|
import uuid
|
||||||
|
import collections
|
||||||
|
|
||||||
|
import ayon_api
|
||||||
|
from ayon_api.graphql import GraphQlQuery
|
||||||
|
from ayon_core.host import ILoadHost
|
||||||
|
|
||||||
|
|
||||||
|
# --- Implementation that should be in ayon-python-api ---
|
||||||
|
# The implementation is not available in all versions of ayon-python-api.
|
||||||
|
RepresentationHierarchy = collections.namedtuple(
|
||||||
|
"RepresentationHierarchy",
|
||||||
|
("folder", "product", "version", "representation")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def representations_parent_ids_qraphql_query():
|
||||||
|
query = GraphQlQuery("RepresentationsHierarchyQuery")
|
||||||
|
|
||||||
|
project_name_var = query.add_variable("projectName", "String!")
|
||||||
|
repre_ids_var = query.add_variable("representationIds", "[String!]")
|
||||||
|
|
||||||
|
project_field = query.add_field("project")
|
||||||
|
project_field.set_filter("name", project_name_var)
|
||||||
|
|
||||||
|
repres_field = project_field.add_field_with_edges("representations")
|
||||||
|
repres_field.add_field("id")
|
||||||
|
repres_field.add_field("name")
|
||||||
|
repres_field.set_filter("ids", repre_ids_var)
|
||||||
|
version_field = repres_field.add_field("version")
|
||||||
|
version_field.add_field("id")
|
||||||
|
product_field = version_field.add_field("product")
|
||||||
|
product_field.add_field("id")
|
||||||
|
product_field.add_field("name")
|
||||||
|
product_field.add_field("productType")
|
||||||
|
product_attrib_field = product_field.add_field("attrib")
|
||||||
|
product_attrib_field.add_field("productGroup")
|
||||||
|
folder_field = product_field.add_field("folder")
|
||||||
|
folder_field.add_field("id")
|
||||||
|
folder_field.add_field("path")
|
||||||
|
return query
|
||||||
|
|
||||||
|
|
||||||
|
def get_representations_hierarchy(project_name, representation_ids):
|
||||||
|
"""Find representations parents by representation id.
|
||||||
|
|
||||||
|
Representation parent entities up to project.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
project_name (str): Project where to look for entities.
|
||||||
|
representation_ids (Iterable[str]): Representation ids.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict[str, RepresentationParents]: Parent entities by
|
||||||
|
representation id.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not representation_ids:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
repre_ids = set(representation_ids)
|
||||||
|
output = {
|
||||||
|
repre_id: RepresentationHierarchy(None, None, None, None)
|
||||||
|
for repre_id in representation_ids
|
||||||
|
}
|
||||||
|
|
||||||
|
query = representations_parent_ids_qraphql_query()
|
||||||
|
query.set_variable_value("projectName", project_name)
|
||||||
|
query.set_variable_value("representationIds", list(repre_ids))
|
||||||
|
|
||||||
|
con = ayon_api.get_server_api_connection()
|
||||||
|
parsed_data = query.query(con)
|
||||||
|
for repre in parsed_data["project"]["representations"]:
|
||||||
|
repre_id = repre["id"]
|
||||||
|
version = repre.pop("version")
|
||||||
|
product = version.pop("product")
|
||||||
|
folder = product.pop("folder")
|
||||||
|
|
||||||
|
output[repre_id] = RepresentationHierarchy(
|
||||||
|
folder, product, version, repre
|
||||||
|
)
|
||||||
|
|
||||||
|
return output
|
||||||
|
# --- END of ayon-python-api implementation ---
|
||||||
|
|
||||||
|
|
||||||
|
class ContainerItem:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
representation_id,
|
||||||
|
loader_name,
|
||||||
|
namespace,
|
||||||
|
name,
|
||||||
|
object_name,
|
||||||
|
item_id
|
||||||
|
):
|
||||||
|
self.representation_id = representation_id
|
||||||
|
self.loader_name = loader_name
|
||||||
|
self.object_name = object_name
|
||||||
|
self.namespace = namespace
|
||||||
|
self.name = name
|
||||||
|
self.item_id = item_id
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_container_data(cls, container):
|
||||||
|
return cls(
|
||||||
|
representation_id=container["representation"],
|
||||||
|
loader_name=container["loader"],
|
||||||
|
namespace=container["namespace"],
|
||||||
|
name=container["name"],
|
||||||
|
object_name=container["objectName"],
|
||||||
|
item_id=uuid.uuid4().hex,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RepresentationInfo:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
folder_id,
|
||||||
|
folder_path,
|
||||||
|
product_id,
|
||||||
|
product_name,
|
||||||
|
product_type,
|
||||||
|
product_group,
|
||||||
|
version_id,
|
||||||
|
representation_name,
|
||||||
|
):
|
||||||
|
self.folder_id = folder_id
|
||||||
|
self.folder_path = folder_path
|
||||||
|
self.product_id = product_id
|
||||||
|
self.product_name = product_name
|
||||||
|
self.product_type = product_type
|
||||||
|
self.product_group = product_group
|
||||||
|
self.version_id = version_id
|
||||||
|
self.representation_name = representation_name
|
||||||
|
self._is_valid = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_valid(self):
|
||||||
|
if self._is_valid is None:
|
||||||
|
self._is_valid = (
|
||||||
|
self.folder_id is not None
|
||||||
|
and self.product_id is not None
|
||||||
|
and self.version_id is not None
|
||||||
|
and self.representation_name is not None
|
||||||
|
)
|
||||||
|
return self._is_valid
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def new_invalid(cls):
|
||||||
|
return cls(None, None, None, None, None, None, None, None)
|
||||||
|
|
||||||
|
|
||||||
|
class VersionItem:
|
||||||
|
def __init__(self, version_id, product_id, version, status, is_latest):
|
||||||
|
self.version = version
|
||||||
|
self.version_id = version_id
|
||||||
|
self.product_id = product_id
|
||||||
|
self.version = version
|
||||||
|
self.status = status
|
||||||
|
self.is_latest = is_latest
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_hero(self):
|
||||||
|
return self.version < 0
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_entity(cls, version_entity, is_latest):
|
||||||
|
return cls(
|
||||||
|
version_id=version_entity["id"],
|
||||||
|
product_id=version_entity["productId"],
|
||||||
|
version=version_entity["version"],
|
||||||
|
status=version_entity["status"],
|
||||||
|
is_latest=is_latest,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ContainersModel:
|
||||||
|
def __init__(self, controller):
|
||||||
|
self._controller = controller
|
||||||
|
self._items_cache = None
|
||||||
|
self._containers_by_id = {}
|
||||||
|
self._container_items_by_id = {}
|
||||||
|
self._version_items_by_product_id = {}
|
||||||
|
self._repre_info_by_id = {}
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self._items_cache = None
|
||||||
|
self._containers_by_id = {}
|
||||||
|
self._container_items_by_id = {}
|
||||||
|
self._version_items_by_product_id = {}
|
||||||
|
self._repre_info_by_id = {}
|
||||||
|
|
||||||
|
def get_containers(self):
|
||||||
|
self._update_cache()
|
||||||
|
return list(self._containers_by_id.values())
|
||||||
|
|
||||||
|
def get_containers_by_item_ids(self, item_ids):
|
||||||
|
return {
|
||||||
|
item_id: self._containers_by_id.get(item_id)
|
||||||
|
for item_id in item_ids
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_container_items(self):
|
||||||
|
self._update_cache()
|
||||||
|
return list(self._items_cache)
|
||||||
|
|
||||||
|
def get_container_items_by_id(self, item_ids):
|
||||||
|
return {
|
||||||
|
item_id: self._container_items_by_id.get(item_id)
|
||||||
|
for item_id in item_ids
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_representation_info_items(self, representation_ids):
|
||||||
|
output = {}
|
||||||
|
missing_repre_ids = set()
|
||||||
|
for repre_id in representation_ids:
|
||||||
|
try:
|
||||||
|
uuid.UUID(repre_id)
|
||||||
|
except ValueError:
|
||||||
|
output[repre_id] = RepresentationInfo.new_invalid()
|
||||||
|
continue
|
||||||
|
|
||||||
|
repre_info = self._repre_info_by_id.get(repre_id)
|
||||||
|
if repre_info is None:
|
||||||
|
missing_repre_ids.add(repre_id)
|
||||||
|
else:
|
||||||
|
output[repre_id] = repre_info
|
||||||
|
|
||||||
|
if not missing_repre_ids:
|
||||||
|
return output
|
||||||
|
|
||||||
|
project_name = self._controller.get_current_project_name()
|
||||||
|
repre_hierarchy_by_id = get_representations_hierarchy(
|
||||||
|
project_name, missing_repre_ids
|
||||||
|
)
|
||||||
|
for repre_id, repre_hierarchy in repre_hierarchy_by_id.items():
|
||||||
|
kwargs = {
|
||||||
|
"folder_id": None,
|
||||||
|
"folder_path": None,
|
||||||
|
"product_id": None,
|
||||||
|
"product_name": None,
|
||||||
|
"product_type": None,
|
||||||
|
"product_group": None,
|
||||||
|
"version_id": None,
|
||||||
|
"representation_name": None,
|
||||||
|
}
|
||||||
|
folder = repre_hierarchy.folder
|
||||||
|
product = repre_hierarchy.product
|
||||||
|
version = repre_hierarchy.version
|
||||||
|
repre = repre_hierarchy.representation
|
||||||
|
if folder:
|
||||||
|
kwargs["folder_id"] = folder["id"]
|
||||||
|
kwargs["folder_path"] = folder["path"]
|
||||||
|
if product:
|
||||||
|
group = product["attrib"]["productGroup"]
|
||||||
|
kwargs["product_id"] = product["id"]
|
||||||
|
kwargs["product_name"] = product["name"]
|
||||||
|
kwargs["product_type"] = product["productType"]
|
||||||
|
kwargs["product_group"] = group
|
||||||
|
if version:
|
||||||
|
kwargs["version_id"] = version["id"]
|
||||||
|
if repre:
|
||||||
|
kwargs["representation_name"] = repre["name"]
|
||||||
|
|
||||||
|
repre_info = RepresentationInfo(**kwargs)
|
||||||
|
self._repre_info_by_id[repre_id] = repre_info
|
||||||
|
output[repre_id] = repre_info
|
||||||
|
return output
|
||||||
|
|
||||||
|
def get_version_items(self, product_ids):
|
||||||
|
if not product_ids:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
missing_ids = {
|
||||||
|
product_id
|
||||||
|
for product_id in product_ids
|
||||||
|
if product_id not in self._version_items_by_product_id
|
||||||
|
}
|
||||||
|
if missing_ids:
|
||||||
|
def version_sorted(entity):
|
||||||
|
return entity["version"]
|
||||||
|
|
||||||
|
project_name = self._controller.get_current_project_name()
|
||||||
|
version_entities_by_product_id = {
|
||||||
|
product_id: []
|
||||||
|
for product_id in missing_ids
|
||||||
|
}
|
||||||
|
|
||||||
|
version_entities = list(ayon_api.get_versions(
|
||||||
|
project_name,
|
||||||
|
product_ids=missing_ids,
|
||||||
|
fields={"id", "version", "productId", "status"}
|
||||||
|
))
|
||||||
|
version_entities.sort(key=version_sorted)
|
||||||
|
for version_entity in version_entities:
|
||||||
|
product_id = version_entity["productId"]
|
||||||
|
version_entities_by_product_id[product_id].append(
|
||||||
|
version_entity
|
||||||
|
)
|
||||||
|
|
||||||
|
for product_id, version_entities in (
|
||||||
|
version_entities_by_product_id.items()
|
||||||
|
):
|
||||||
|
last_version = abs(version_entities[-1]["version"])
|
||||||
|
version_items_by_id = {
|
||||||
|
entity["id"]: VersionItem.from_entity(
|
||||||
|
entity, abs(entity["version"]) == last_version
|
||||||
|
)
|
||||||
|
for entity in version_entities
|
||||||
|
}
|
||||||
|
self._version_items_by_product_id[product_id] = (
|
||||||
|
version_items_by_id
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
product_id: dict(self._version_items_by_product_id[product_id])
|
||||||
|
for product_id in product_ids
|
||||||
|
}
|
||||||
|
|
||||||
|
def _update_cache(self):
|
||||||
|
if self._items_cache is not None:
|
||||||
|
return
|
||||||
|
|
||||||
|
host = self._controller.get_host()
|
||||||
|
if isinstance(host, ILoadHost):
|
||||||
|
containers = list(host.get_containers())
|
||||||
|
elif hasattr(host, "ls"):
|
||||||
|
containers = list(host.ls())
|
||||||
|
else:
|
||||||
|
containers = []
|
||||||
|
container_items = []
|
||||||
|
containers_by_id = {}
|
||||||
|
container_items_by_id = {}
|
||||||
|
for container in containers:
|
||||||
|
item = ContainerItem.from_container_data(container)
|
||||||
|
containers_by_id[item.item_id] = container
|
||||||
|
container_items_by_id[item.item_id] = item
|
||||||
|
container_items.append(item)
|
||||||
|
|
||||||
|
self._containers_by_id = containers_by_id
|
||||||
|
self._container_items_by_id = container_items_by_id
|
||||||
|
self._items_cache = container_items
|
||||||
216
client/ayon_core/tools/sceneinventory/select_version_dialog.py
Normal file
216
client/ayon_core/tools/sceneinventory/select_version_dialog.py
Normal file
|
|
@ -0,0 +1,216 @@
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from qtpy import QtWidgets, QtCore, QtGui
|
||||||
|
|
||||||
|
from ayon_core.tools.utils.delegates import StatusDelegate
|
||||||
|
|
||||||
|
from .model import (
|
||||||
|
ITEM_ID_ROLE,
|
||||||
|
STATUS_NAME_ROLE,
|
||||||
|
STATUS_SHORT_ROLE,
|
||||||
|
STATUS_COLOR_ROLE,
|
||||||
|
STATUS_ICON_ROLE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class VersionOption:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
version,
|
||||||
|
label,
|
||||||
|
status_name,
|
||||||
|
status_short,
|
||||||
|
status_color
|
||||||
|
):
|
||||||
|
self.version = version
|
||||||
|
self.label = label
|
||||||
|
self.status_name = status_name
|
||||||
|
self.status_short = status_short
|
||||||
|
self.status_color = status_color
|
||||||
|
|
||||||
|
|
||||||
|
class SelectVersionModel(QtGui.QStandardItemModel):
|
||||||
|
def data(self, index, role=None):
|
||||||
|
if role is None:
|
||||||
|
role = QtCore.Qt.DisplayRole
|
||||||
|
|
||||||
|
index = self.index(index.row(), 0, index.parent())
|
||||||
|
return super().data(index, role)
|
||||||
|
|
||||||
|
|
||||||
|
class SelectVersionComboBox(QtWidgets.QComboBox):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
combo_model = SelectVersionModel(0, 2)
|
||||||
|
|
||||||
|
self.setModel(combo_model)
|
||||||
|
|
||||||
|
combo_view = QtWidgets.QTreeView(self)
|
||||||
|
combo_view.setHeaderHidden(True)
|
||||||
|
combo_view.setIndentation(0)
|
||||||
|
|
||||||
|
self.setView(combo_view)
|
||||||
|
|
||||||
|
header = combo_view.header()
|
||||||
|
header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
|
||||||
|
header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
|
||||||
|
|
||||||
|
status_delegate = StatusDelegate(
|
||||||
|
STATUS_NAME_ROLE,
|
||||||
|
STATUS_SHORT_ROLE,
|
||||||
|
STATUS_COLOR_ROLE,
|
||||||
|
STATUS_ICON_ROLE,
|
||||||
|
)
|
||||||
|
combo_view.setItemDelegateForColumn(1, status_delegate)
|
||||||
|
|
||||||
|
self._combo_model = combo_model
|
||||||
|
self._combo_view = combo_view
|
||||||
|
self._status_delegate = status_delegate
|
||||||
|
self._items_by_id = {}
|
||||||
|
|
||||||
|
def paintEvent(self, event):
|
||||||
|
painter = QtWidgets.QStylePainter(self)
|
||||||
|
option = QtWidgets.QStyleOptionComboBox()
|
||||||
|
self.initStyleOption(option)
|
||||||
|
painter.drawComplexControl(QtWidgets.QStyle.CC_ComboBox, option)
|
||||||
|
idx = self.currentIndex()
|
||||||
|
status_name = self.itemData(idx, STATUS_NAME_ROLE)
|
||||||
|
if status_name is None:
|
||||||
|
painter.drawControl(QtWidgets.QStyle.CE_ComboBoxLabel, option)
|
||||||
|
return
|
||||||
|
|
||||||
|
painter.save()
|
||||||
|
text_field_rect = self.style().subControlRect(
|
||||||
|
QtWidgets.QStyle.CC_ComboBox,
|
||||||
|
option,
|
||||||
|
QtWidgets.QStyle.SC_ComboBoxEditField
|
||||||
|
)
|
||||||
|
adj_rect = text_field_rect.adjusted(1, 0, -1, 0)
|
||||||
|
painter.drawText(
|
||||||
|
adj_rect,
|
||||||
|
QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter,
|
||||||
|
option.currentText
|
||||||
|
)
|
||||||
|
metrics = QtGui.QFontMetrics(self.font())
|
||||||
|
text_width = metrics.width(option.currentText)
|
||||||
|
x_offset = text_width + 2
|
||||||
|
diff_width = adj_rect.width() - x_offset
|
||||||
|
if diff_width <= 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
status_rect = adj_rect.adjusted(x_offset + 2, 0, 0, 0)
|
||||||
|
if diff_width < metrics.width(status_name):
|
||||||
|
status_name = self.itemData(idx, STATUS_SHORT_ROLE)
|
||||||
|
|
||||||
|
color = QtGui.QColor(self.itemData(idx, STATUS_COLOR_ROLE))
|
||||||
|
|
||||||
|
pen = painter.pen()
|
||||||
|
pen.setColor(color)
|
||||||
|
painter.setPen(pen)
|
||||||
|
painter.drawText(
|
||||||
|
status_rect,
|
||||||
|
QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter,
|
||||||
|
status_name
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_current_index(self, index):
|
||||||
|
model = self._combo_view.model()
|
||||||
|
if index > model.rowCount():
|
||||||
|
return
|
||||||
|
|
||||||
|
self.setCurrentIndex(index)
|
||||||
|
|
||||||
|
def get_item_by_id(self, item_id):
|
||||||
|
return self._items_by_id[item_id]
|
||||||
|
|
||||||
|
def set_versions(self, version_options):
|
||||||
|
self._items_by_id = {}
|
||||||
|
model = self._combo_model
|
||||||
|
root_item = model.invisibleRootItem()
|
||||||
|
root_item.removeRows(0, root_item.rowCount())
|
||||||
|
|
||||||
|
new_items = []
|
||||||
|
for version_option in version_options:
|
||||||
|
item_id = uuid.uuid4().hex
|
||||||
|
item = QtGui.QStandardItem(version_option.label)
|
||||||
|
item.setColumnCount(root_item.columnCount())
|
||||||
|
item.setData(
|
||||||
|
version_option.status_name, STATUS_NAME_ROLE
|
||||||
|
)
|
||||||
|
item.setData(
|
||||||
|
version_option.status_short, STATUS_SHORT_ROLE
|
||||||
|
)
|
||||||
|
item.setData(
|
||||||
|
version_option.status_color, STATUS_COLOR_ROLE
|
||||||
|
)
|
||||||
|
item.setData(item_id, ITEM_ID_ROLE)
|
||||||
|
|
||||||
|
new_items.append(item)
|
||||||
|
self._items_by_id[item_id] = version_option
|
||||||
|
|
||||||
|
if new_items:
|
||||||
|
root_item.appendRows(new_items)
|
||||||
|
|
||||||
|
|
||||||
|
class SelectVersionDialog(QtWidgets.QDialog):
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent=parent)
|
||||||
|
|
||||||
|
self.setWindowTitle("Select version")
|
||||||
|
|
||||||
|
label_widget = QtWidgets.QLabel("Set version number to", self)
|
||||||
|
versions_combobox = SelectVersionComboBox(self)
|
||||||
|
|
||||||
|
btns_widget = QtWidgets.QWidget(self)
|
||||||
|
|
||||||
|
confirm_btn = QtWidgets.QPushButton("OK", btns_widget)
|
||||||
|
cancel_btn = QtWidgets.QPushButton("Cancel", btns_widget)
|
||||||
|
|
||||||
|
btns_layout = QtWidgets.QHBoxLayout(btns_widget)
|
||||||
|
btns_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
btns_layout.addStretch(1)
|
||||||
|
btns_layout.addWidget(confirm_btn, 0)
|
||||||
|
btns_layout.addWidget(cancel_btn, 0)
|
||||||
|
|
||||||
|
main_layout = QtWidgets.QVBoxLayout(self)
|
||||||
|
main_layout.addWidget(label_widget, 0)
|
||||||
|
main_layout.addWidget(versions_combobox, 0)
|
||||||
|
main_layout.addWidget(btns_widget, 0)
|
||||||
|
|
||||||
|
confirm_btn.clicked.connect(self._on_confirm)
|
||||||
|
cancel_btn.clicked.connect(self._on_cancel)
|
||||||
|
|
||||||
|
self._selected_item = None
|
||||||
|
self._cancelled = False
|
||||||
|
self._versions_combobox = versions_combobox
|
||||||
|
|
||||||
|
def get_selected_item(self):
|
||||||
|
if self._cancelled:
|
||||||
|
return None
|
||||||
|
return self._selected_item
|
||||||
|
|
||||||
|
def set_versions(self, version_options):
|
||||||
|
self._versions_combobox.set_versions(version_options)
|
||||||
|
|
||||||
|
def select_index(self, index):
|
||||||
|
self._versions_combobox.set_current_index(index)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def ask_for_version(cls, version_options, index=None, parent=None):
|
||||||
|
dialog = cls(parent)
|
||||||
|
dialog.set_versions(version_options)
|
||||||
|
if index is not None:
|
||||||
|
dialog.select_index(index)
|
||||||
|
dialog.exec_()
|
||||||
|
return dialog.get_selected_item()
|
||||||
|
|
||||||
|
def _on_confirm(self):
|
||||||
|
self._cancelled = False
|
||||||
|
index = self._versions_combobox.currentIndex()
|
||||||
|
item_id = self._versions_combobox.itemData(index, ITEM_ID_ROLE)
|
||||||
|
self._selected_item = self._versions_combobox.get_item_by_id(item_id)
|
||||||
|
self.accept()
|
||||||
|
|
||||||
|
def _on_cancel(self):
|
||||||
|
self._cancelled = True
|
||||||
|
self.reject()
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -2,17 +2,10 @@ from qtpy import QtWidgets, QtCore, QtGui
|
||||||
import qtawesome
|
import qtawesome
|
||||||
|
|
||||||
from ayon_core import style, resources
|
from ayon_core import style, resources
|
||||||
from ayon_core.tools.utils.lib import (
|
from ayon_core.tools.utils import PlaceholderLineEdit
|
||||||
preserve_expanded_rows,
|
|
||||||
preserve_selection,
|
|
||||||
)
|
|
||||||
from ayon_core.tools.sceneinventory import SceneInventoryController
|
from ayon_core.tools.sceneinventory import SceneInventoryController
|
||||||
|
|
||||||
from .delegates import VersionDelegate
|
|
||||||
from .model import (
|
|
||||||
InventoryModel,
|
|
||||||
FilterProxyModel
|
|
||||||
)
|
|
||||||
from .view import SceneInventoryView
|
from .view import SceneInventoryView
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -20,7 +13,7 @@ class SceneInventoryWindow(QtWidgets.QDialog):
|
||||||
"""Scene Inventory window"""
|
"""Scene Inventory window"""
|
||||||
|
|
||||||
def __init__(self, controller=None, parent=None):
|
def __init__(self, controller=None, parent=None):
|
||||||
super(SceneInventoryWindow, self).__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
if controller is None:
|
if controller is None:
|
||||||
controller = SceneInventoryController()
|
controller = SceneInventoryController()
|
||||||
|
|
@ -33,10 +26,9 @@ class SceneInventoryWindow(QtWidgets.QDialog):
|
||||||
|
|
||||||
self.resize(1100, 480)
|
self.resize(1100, 480)
|
||||||
|
|
||||||
# region control
|
|
||||||
|
|
||||||
filter_label = QtWidgets.QLabel("Search", self)
|
filter_label = QtWidgets.QLabel("Search", self)
|
||||||
text_filter = QtWidgets.QLineEdit(self)
|
text_filter = PlaceholderLineEdit(self)
|
||||||
|
text_filter.setPlaceholderText("Filter by name...")
|
||||||
|
|
||||||
outdated_only_checkbox = QtWidgets.QCheckBox(
|
outdated_only_checkbox = QtWidgets.QCheckBox(
|
||||||
"Filter to outdated", self
|
"Filter to outdated", self
|
||||||
|
|
@ -44,52 +36,30 @@ class SceneInventoryWindow(QtWidgets.QDialog):
|
||||||
outdated_only_checkbox.setToolTip("Show outdated files only")
|
outdated_only_checkbox.setToolTip("Show outdated files only")
|
||||||
outdated_only_checkbox.setChecked(False)
|
outdated_only_checkbox.setChecked(False)
|
||||||
|
|
||||||
icon = qtawesome.icon("fa.arrow-up", color="white")
|
update_all_icon = qtawesome.icon("fa.arrow-up", color="white")
|
||||||
update_all_button = QtWidgets.QPushButton(self)
|
update_all_button = QtWidgets.QPushButton(self)
|
||||||
update_all_button.setToolTip("Update all outdated to latest version")
|
update_all_button.setToolTip("Update all outdated to latest version")
|
||||||
update_all_button.setIcon(icon)
|
update_all_button.setIcon(update_all_icon)
|
||||||
|
|
||||||
icon = qtawesome.icon("fa.refresh", color="white")
|
refresh_icon = qtawesome.icon("fa.refresh", color="white")
|
||||||
refresh_button = QtWidgets.QPushButton(self)
|
refresh_button = QtWidgets.QPushButton(self)
|
||||||
refresh_button.setToolTip("Refresh")
|
refresh_button.setToolTip("Refresh")
|
||||||
refresh_button.setIcon(icon)
|
refresh_button.setIcon(refresh_icon)
|
||||||
|
|
||||||
control_layout = QtWidgets.QHBoxLayout()
|
headers_widget = QtWidgets.QWidget(self)
|
||||||
control_layout.addWidget(filter_label)
|
headers_layout = QtWidgets.QHBoxLayout(headers_widget)
|
||||||
control_layout.addWidget(text_filter)
|
headers_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
control_layout.addWidget(outdated_only_checkbox)
|
headers_layout.addWidget(filter_label, 0)
|
||||||
control_layout.addWidget(update_all_button)
|
headers_layout.addWidget(text_filter, 1)
|
||||||
control_layout.addWidget(refresh_button)
|
headers_layout.addWidget(outdated_only_checkbox, 0)
|
||||||
|
headers_layout.addWidget(update_all_button, 0)
|
||||||
model = InventoryModel(controller)
|
headers_layout.addWidget(refresh_button, 0)
|
||||||
proxy = FilterProxyModel()
|
|
||||||
proxy.setSourceModel(model)
|
|
||||||
proxy.setDynamicSortFilter(True)
|
|
||||||
proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
|
|
||||||
|
|
||||||
view = SceneInventoryView(controller, self)
|
view = SceneInventoryView(controller, self)
|
||||||
view.setModel(proxy)
|
|
||||||
|
|
||||||
sync_enabled = controller.is_sitesync_enabled()
|
main_layout = QtWidgets.QVBoxLayout(self)
|
||||||
view.setColumnHidden(model.active_site_col, not sync_enabled)
|
main_layout.addWidget(headers_widget, 0)
|
||||||
view.setColumnHidden(model.remote_site_col, not sync_enabled)
|
main_layout.addWidget(view, 1)
|
||||||
|
|
||||||
# set some nice default widths for the view
|
|
||||||
view.setColumnWidth(0, 250) # name
|
|
||||||
view.setColumnWidth(1, 55) # version
|
|
||||||
view.setColumnWidth(2, 55) # count
|
|
||||||
view.setColumnWidth(3, 150) # product type
|
|
||||||
view.setColumnWidth(4, 120) # group
|
|
||||||
view.setColumnWidth(5, 150) # loader
|
|
||||||
|
|
||||||
# apply delegates
|
|
||||||
version_delegate = VersionDelegate(controller, self)
|
|
||||||
column = model.Columns.index("version")
|
|
||||||
view.setItemDelegateForColumn(column, version_delegate)
|
|
||||||
|
|
||||||
layout = QtWidgets.QVBoxLayout(self)
|
|
||||||
layout.addLayout(control_layout)
|
|
||||||
layout.addWidget(view)
|
|
||||||
|
|
||||||
show_timer = QtCore.QTimer()
|
show_timer = QtCore.QTimer()
|
||||||
show_timer.setInterval(0)
|
show_timer.setInterval(0)
|
||||||
|
|
@ -114,12 +84,8 @@ class SceneInventoryWindow(QtWidgets.QDialog):
|
||||||
self._update_all_button = update_all_button
|
self._update_all_button = update_all_button
|
||||||
self._outdated_only_checkbox = outdated_only_checkbox
|
self._outdated_only_checkbox = outdated_only_checkbox
|
||||||
self._view = view
|
self._view = view
|
||||||
self._model = model
|
|
||||||
self._proxy = proxy
|
|
||||||
self._version_delegate = version_delegate
|
|
||||||
|
|
||||||
self._first_show = True
|
self._first_show = True
|
||||||
self._first_refresh = True
|
|
||||||
|
|
||||||
def showEvent(self, event):
|
def showEvent(self, event):
|
||||||
super(SceneInventoryWindow, self).showEvent(event)
|
super(SceneInventoryWindow, self).showEvent(event)
|
||||||
|
|
@ -139,29 +105,16 @@ class SceneInventoryWindow(QtWidgets.QDialog):
|
||||||
whilst trying to name an instance.
|
whilst trying to name an instance.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
def _on_refresh_request(self):
|
def _on_refresh_request(self):
|
||||||
"""Signal callback to trigger 'refresh' without any arguments."""
|
"""Signal callback to trigger 'refresh' without any arguments."""
|
||||||
|
|
||||||
self.refresh()
|
self.refresh()
|
||||||
|
|
||||||
def refresh(self, containers=None):
|
def refresh(self):
|
||||||
self._first_refresh = False
|
|
||||||
self._controller.reset()
|
self._controller.reset()
|
||||||
with preserve_expanded_rows(
|
self._view.refresh()
|
||||||
tree_view=self._view,
|
|
||||||
role=self._model.UniqueRole
|
|
||||||
):
|
|
||||||
with preserve_selection(
|
|
||||||
tree_view=self._view,
|
|
||||||
role=self._model.UniqueRole,
|
|
||||||
current_index=False
|
|
||||||
):
|
|
||||||
kwargs = {"containers": containers}
|
|
||||||
# TODO do not touch view's inner attribute
|
|
||||||
if self._view._hierarchy_view:
|
|
||||||
kwargs["selected"] = self._view._selected
|
|
||||||
self._model.refresh(**kwargs)
|
|
||||||
|
|
||||||
def _on_show_timer(self):
|
def _on_show_timer(self):
|
||||||
if self._show_counter < 3:
|
if self._show_counter < 3:
|
||||||
|
|
@ -171,17 +124,13 @@ class SceneInventoryWindow(QtWidgets.QDialog):
|
||||||
self.refresh()
|
self.refresh()
|
||||||
|
|
||||||
def _on_hierarchy_view_change(self, enabled):
|
def _on_hierarchy_view_change(self, enabled):
|
||||||
self._proxy.set_hierarchy_view(enabled)
|
self._view.set_hierarchy_view(enabled)
|
||||||
self._model.set_hierarchy_view(enabled)
|
|
||||||
|
|
||||||
def _on_text_filter_change(self, text_filter):
|
def _on_text_filter_change(self, text_filter):
|
||||||
if hasattr(self._proxy, "setFilterRegExp"):
|
self._view.set_text_filter(text_filter)
|
||||||
self._proxy.setFilterRegExp(text_filter)
|
|
||||||
else:
|
|
||||||
self._proxy.setFilterRegularExpression(text_filter)
|
|
||||||
|
|
||||||
def _on_outdated_state_change(self):
|
def _on_outdated_state_change(self):
|
||||||
self._proxy.set_filter_outdated(
|
self._view.set_filter_outdated(
|
||||||
self._outdated_only_checkbox.isChecked()
|
self._outdated_only_checkbox.isChecked()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
from .broker import StdOutBroker
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
"StdOutBroker",
|
||||||
|
)
|
||||||
|
|
@ -1,173 +1,12 @@
|
||||||
import os
|
import warnings
|
||||||
import sys
|
from .broker import StdOutBroker
|
||||||
import threading
|
|
||||||
import collections
|
|
||||||
import websocket
|
|
||||||
import json
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from ayon_core.lib import Logger
|
warnings.warn(
|
||||||
from openpype_modules.webserver.host_console_listener import MsgAction
|
(
|
||||||
|
"Import of 'StdOutBroker' from 'ayon_core.tools.stdout_broker.app'"
|
||||||
|
" is deprecated. Please use 'ayon_core.tools.stdout_broker' instead."
|
||||||
|
),
|
||||||
|
DeprecationWarning
|
||||||
|
)
|
||||||
|
|
||||||
log = Logger.get_logger(__name__)
|
__all__ = ("StdOutBroker", )
|
||||||
|
|
||||||
|
|
||||||
class StdOutBroker:
|
|
||||||
"""
|
|
||||||
Application showing console in Services tray for non python hosts
|
|
||||||
instead of cmd window.
|
|
||||||
"""
|
|
||||||
MAX_LINES = 10000
|
|
||||||
TIMER_TIMEOUT = 0.200
|
|
||||||
|
|
||||||
def __init__(self, host_name):
|
|
||||||
self.host_name = host_name
|
|
||||||
self.webserver_client = None
|
|
||||||
|
|
||||||
self.original_stdout_write = None
|
|
||||||
self.original_stderr_write = None
|
|
||||||
self.log_queue = collections.deque()
|
|
||||||
|
|
||||||
date_str = datetime.now().strftime("%d%m%Y%H%M%S")
|
|
||||||
self.host_id = "{}_{}".format(self.host_name, date_str)
|
|
||||||
|
|
||||||
self._std_available = False
|
|
||||||
self._is_running = False
|
|
||||||
self._catch_std_outputs()
|
|
||||||
|
|
||||||
self._timer = None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def send_to_tray(self):
|
|
||||||
"""Checks if connected to tray and have access to logs."""
|
|
||||||
return self.webserver_client and self._std_available
|
|
||||||
|
|
||||||
def start(self):
|
|
||||||
"""Start app, create and start timer"""
|
|
||||||
if not self._std_available or self._is_running:
|
|
||||||
return
|
|
||||||
self._is_running = True
|
|
||||||
self._create_timer()
|
|
||||||
self._connect_to_tray()
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
"""Disconnect from Tray, process last logs"""
|
|
||||||
if not self._is_running:
|
|
||||||
return
|
|
||||||
self._is_running = False
|
|
||||||
self._process_queue()
|
|
||||||
self._disconnect_from_tray()
|
|
||||||
|
|
||||||
def host_connected(self):
|
|
||||||
"""Send to Tray console that host is ready - icon change. """
|
|
||||||
log.info("Host {} connected".format(self.host_id))
|
|
||||||
|
|
||||||
payload = {
|
|
||||||
"host": self.host_id,
|
|
||||||
"action": MsgAction.INITIALIZED,
|
|
||||||
"text": "Integration with {}".format(
|
|
||||||
str.capitalize(self.host_name))
|
|
||||||
}
|
|
||||||
self._send(payload)
|
|
||||||
|
|
||||||
def _create_timer(self):
|
|
||||||
timer = threading.Timer(self.TIMER_TIMEOUT, self._timer_callback)
|
|
||||||
timer.start()
|
|
||||||
self._timer = timer
|
|
||||||
|
|
||||||
def _timer_callback(self):
|
|
||||||
if not self._is_running:
|
|
||||||
return
|
|
||||||
self._process_queue()
|
|
||||||
self._create_timer()
|
|
||||||
|
|
||||||
def _connect_to_tray(self):
|
|
||||||
"""Connect to Tray webserver to pass console output. """
|
|
||||||
if not self._std_available: # not content to log
|
|
||||||
return
|
|
||||||
ws = websocket.WebSocket()
|
|
||||||
webserver_url = os.environ.get("AYON_WEBSERVER_URL")
|
|
||||||
|
|
||||||
if not webserver_url:
|
|
||||||
print("Unknown webserver url, cannot connect to pass log")
|
|
||||||
return
|
|
||||||
|
|
||||||
webserver_url = webserver_url.replace("http", "ws")
|
|
||||||
ws.connect("{}/ws/host_listener".format(webserver_url))
|
|
||||||
self.webserver_client = ws
|
|
||||||
|
|
||||||
payload = {
|
|
||||||
"host": self.host_id,
|
|
||||||
"action": MsgAction.CONNECTING,
|
|
||||||
"text": "Integration with {}".format(
|
|
||||||
str.capitalize(self.host_name))
|
|
||||||
}
|
|
||||||
self._send(payload)
|
|
||||||
|
|
||||||
def _disconnect_from_tray(self):
|
|
||||||
"""Send to Tray that host is closing - remove from Services. """
|
|
||||||
print("Host {} closing".format(self.host_name))
|
|
||||||
if not self.webserver_client:
|
|
||||||
return
|
|
||||||
|
|
||||||
payload = {
|
|
||||||
"host": self.host_id,
|
|
||||||
"action": MsgAction.CLOSE,
|
|
||||||
"text": "Integration with {}".format(
|
|
||||||
str.capitalize(self.host_name))
|
|
||||||
}
|
|
||||||
|
|
||||||
self._send(payload)
|
|
||||||
self.webserver_client.close()
|
|
||||||
|
|
||||||
def _catch_std_outputs(self):
|
|
||||||
"""Redirects standard out and error to own functions"""
|
|
||||||
if sys.stdout:
|
|
||||||
self.original_stdout_write = sys.stdout.write
|
|
||||||
sys.stdout.write = self._my_stdout_write
|
|
||||||
self._std_available = True
|
|
||||||
|
|
||||||
if sys.stderr:
|
|
||||||
self.original_stderr_write = sys.stderr.write
|
|
||||||
sys.stderr.write = self._my_stderr_write
|
|
||||||
self._std_available = True
|
|
||||||
|
|
||||||
def _my_stdout_write(self, text):
|
|
||||||
"""Appends outputted text to queue, keep writing to original stdout"""
|
|
||||||
if self.original_stdout_write is not None:
|
|
||||||
self.original_stdout_write(text)
|
|
||||||
if self.send_to_tray:
|
|
||||||
self.log_queue.append(text)
|
|
||||||
|
|
||||||
def _my_stderr_write(self, text):
|
|
||||||
"""Appends outputted text to queue, keep writing to original stderr"""
|
|
||||||
if self.original_stderr_write is not None:
|
|
||||||
self.original_stderr_write(text)
|
|
||||||
if self.send_to_tray:
|
|
||||||
self.log_queue.append(text)
|
|
||||||
|
|
||||||
def _process_queue(self):
|
|
||||||
"""Sends lines and purges queue"""
|
|
||||||
if not self.send_to_tray:
|
|
||||||
return
|
|
||||||
|
|
||||||
lines = tuple(self.log_queue)
|
|
||||||
self.log_queue.clear()
|
|
||||||
if lines:
|
|
||||||
payload = {
|
|
||||||
"host": self.host_id,
|
|
||||||
"action": MsgAction.ADD,
|
|
||||||
"text": "\n".join(lines)
|
|
||||||
}
|
|
||||||
|
|
||||||
self._send(payload)
|
|
||||||
|
|
||||||
def _send(self, payload):
|
|
||||||
"""Worker method to send to existing websocket connection."""
|
|
||||||
if not self.send_to_tray:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.webserver_client.send(json.dumps(payload))
|
|
||||||
except ConnectionResetError: # Tray closed
|
|
||||||
self._connect_to_tray()
|
|
||||||
|
|
|
||||||
174
client/ayon_core/tools/stdout_broker/broker.py
Normal file
174
client/ayon_core/tools/stdout_broker/broker.py
Normal file
|
|
@ -0,0 +1,174 @@
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import collections
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import websocket
|
||||||
|
|
||||||
|
from ayon_core.lib import Logger
|
||||||
|
from ayon_core.modules.webserver import HostMsgAction
|
||||||
|
|
||||||
|
log = Logger.get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class StdOutBroker:
|
||||||
|
"""
|
||||||
|
Application showing console in Services tray for non python hosts
|
||||||
|
instead of cmd window.
|
||||||
|
"""
|
||||||
|
MAX_LINES = 10000
|
||||||
|
TIMER_TIMEOUT = 0.200
|
||||||
|
|
||||||
|
def __init__(self, host_name):
|
||||||
|
self.host_name = host_name
|
||||||
|
self.webserver_client = None
|
||||||
|
|
||||||
|
self.original_stdout_write = None
|
||||||
|
self.original_stderr_write = None
|
||||||
|
self.log_queue = collections.deque()
|
||||||
|
|
||||||
|
date_str = datetime.now().strftime("%d%m%Y%H%M%S")
|
||||||
|
self.host_id = "{}_{}".format(self.host_name, date_str)
|
||||||
|
|
||||||
|
self._std_available = False
|
||||||
|
self._is_running = False
|
||||||
|
self._catch_std_outputs()
|
||||||
|
|
||||||
|
self._timer = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def send_to_tray(self):
|
||||||
|
"""Checks if connected to tray and have access to logs."""
|
||||||
|
return self.webserver_client and self._std_available
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
"""Start app, create and start timer"""
|
||||||
|
if not self._std_available or self._is_running:
|
||||||
|
return
|
||||||
|
self._is_running = True
|
||||||
|
self._create_timer()
|
||||||
|
self._connect_to_tray()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""Disconnect from Tray, process last logs"""
|
||||||
|
if not self._is_running:
|
||||||
|
return
|
||||||
|
self._is_running = False
|
||||||
|
self._process_queue()
|
||||||
|
self._disconnect_from_tray()
|
||||||
|
|
||||||
|
def host_connected(self):
|
||||||
|
"""Send to Tray console that host is ready - icon change. """
|
||||||
|
log.info("Host {} connected".format(self.host_id))
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"host": self.host_id,
|
||||||
|
"action": HostMsgAction.INITIALIZED,
|
||||||
|
"text": "Integration with {}".format(
|
||||||
|
str.capitalize(self.host_name))
|
||||||
|
}
|
||||||
|
self._send(payload)
|
||||||
|
|
||||||
|
def _create_timer(self):
|
||||||
|
timer = threading.Timer(self.TIMER_TIMEOUT, self._timer_callback)
|
||||||
|
timer.start()
|
||||||
|
self._timer = timer
|
||||||
|
|
||||||
|
def _timer_callback(self):
|
||||||
|
if not self._is_running:
|
||||||
|
return
|
||||||
|
self._process_queue()
|
||||||
|
self._create_timer()
|
||||||
|
|
||||||
|
def _connect_to_tray(self):
|
||||||
|
"""Connect to Tray webserver to pass console output. """
|
||||||
|
if not self._std_available: # not content to log
|
||||||
|
return
|
||||||
|
ws = websocket.WebSocket()
|
||||||
|
webserver_url = os.environ.get("AYON_WEBSERVER_URL")
|
||||||
|
|
||||||
|
if not webserver_url:
|
||||||
|
print("Unknown webserver url, cannot connect to pass log")
|
||||||
|
return
|
||||||
|
|
||||||
|
webserver_url = webserver_url.replace("http", "ws")
|
||||||
|
ws.connect("{}/ws/host_listener".format(webserver_url))
|
||||||
|
self.webserver_client = ws
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"host": self.host_id,
|
||||||
|
"action": HostMsgAction.CONNECTING,
|
||||||
|
"text": "Integration with {}".format(
|
||||||
|
str.capitalize(self.host_name))
|
||||||
|
}
|
||||||
|
self._send(payload)
|
||||||
|
|
||||||
|
def _disconnect_from_tray(self):
|
||||||
|
"""Send to Tray that host is closing - remove from Services. """
|
||||||
|
print("Host {} closing".format(self.host_name))
|
||||||
|
if not self.webserver_client:
|
||||||
|
return
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"host": self.host_id,
|
||||||
|
"action": HostMsgAction.CLOSE,
|
||||||
|
"text": "Integration with {}".format(
|
||||||
|
str.capitalize(self.host_name))
|
||||||
|
}
|
||||||
|
|
||||||
|
self._send(payload)
|
||||||
|
self.webserver_client.close()
|
||||||
|
|
||||||
|
def _catch_std_outputs(self):
|
||||||
|
"""Redirects standard out and error to own functions"""
|
||||||
|
if sys.stdout:
|
||||||
|
self.original_stdout_write = sys.stdout.write
|
||||||
|
sys.stdout.write = self._my_stdout_write
|
||||||
|
self._std_available = True
|
||||||
|
|
||||||
|
if sys.stderr:
|
||||||
|
self.original_stderr_write = sys.stderr.write
|
||||||
|
sys.stderr.write = self._my_stderr_write
|
||||||
|
self._std_available = True
|
||||||
|
|
||||||
|
def _my_stdout_write(self, text):
|
||||||
|
"""Appends outputted text to queue, keep writing to original stdout"""
|
||||||
|
if self.original_stdout_write is not None:
|
||||||
|
self.original_stdout_write(text)
|
||||||
|
if self.send_to_tray:
|
||||||
|
self.log_queue.append(text)
|
||||||
|
|
||||||
|
def _my_stderr_write(self, text):
|
||||||
|
"""Appends outputted text to queue, keep writing to original stderr"""
|
||||||
|
if self.original_stderr_write is not None:
|
||||||
|
self.original_stderr_write(text)
|
||||||
|
if self.send_to_tray:
|
||||||
|
self.log_queue.append(text)
|
||||||
|
|
||||||
|
def _process_queue(self):
|
||||||
|
"""Sends lines and purges queue"""
|
||||||
|
if not self.send_to_tray:
|
||||||
|
return
|
||||||
|
|
||||||
|
lines = tuple(self.log_queue)
|
||||||
|
self.log_queue.clear()
|
||||||
|
if lines:
|
||||||
|
payload = {
|
||||||
|
"host": self.host_id,
|
||||||
|
"action": HostMsgAction.ADD,
|
||||||
|
"text": "\n".join(lines)
|
||||||
|
}
|
||||||
|
|
||||||
|
self._send(payload)
|
||||||
|
|
||||||
|
def _send(self, payload):
|
||||||
|
"""Worker method to send to existing websocket connection."""
|
||||||
|
if not self.send_to_tray:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.webserver_client.send(json.dumps(payload))
|
||||||
|
except ConnectionResetError: # Tray closed
|
||||||
|
self._connect_to_tray()
|
||||||
|
|
@ -447,8 +447,10 @@ class SystemTrayIcon(QtWidgets.QSystemTrayIcon):
|
||||||
|
|
||||||
def initialize_addons(self):
|
def initialize_addons(self):
|
||||||
self._initializing_addons = True
|
self._initializing_addons = True
|
||||||
self.tray_man.initialize_addons()
|
try:
|
||||||
self._initializing_addons = False
|
self.tray_man.initialize_addons()
|
||||||
|
finally:
|
||||||
|
self._initializing_addons = False
|
||||||
|
|
||||||
def _click_timer_timeout(self):
|
def _click_timer_timeout(self):
|
||||||
self._click_timer.stop()
|
self._click_timer.stop()
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from qtpy import QtWidgets
|
from qtpy import QtWidgets, QtGui
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -106,3 +106,80 @@ class PrettyTimeDelegate(QtWidgets.QStyledItemDelegate):
|
||||||
def displayText(self, value, locale):
|
def displayText(self, value, locale):
|
||||||
if value is not None:
|
if value is not None:
|
||||||
return pretty_timestamp(value)
|
return pretty_timestamp(value)
|
||||||
|
|
||||||
|
|
||||||
|
class StatusDelegate(QtWidgets.QStyledItemDelegate):
|
||||||
|
"""Delegate showing status name and short name."""
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
status_name_role,
|
||||||
|
status_short_name_role,
|
||||||
|
status_color_role,
|
||||||
|
status_icon_role,
|
||||||
|
*args, **kwargs
|
||||||
|
):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.status_name_role = status_name_role
|
||||||
|
self.status_short_name_role = status_short_name_role
|
||||||
|
self.status_color_role = status_color_role
|
||||||
|
self.status_icon_role = status_icon_role
|
||||||
|
|
||||||
|
def paint(self, painter, option, index):
|
||||||
|
if option.widget:
|
||||||
|
style = option.widget.style()
|
||||||
|
else:
|
||||||
|
style = QtWidgets.QApplication.style()
|
||||||
|
|
||||||
|
style.drawControl(
|
||||||
|
QtWidgets.QCommonStyle.CE_ItemViewItem,
|
||||||
|
option,
|
||||||
|
painter,
|
||||||
|
option.widget
|
||||||
|
)
|
||||||
|
|
||||||
|
painter.save()
|
||||||
|
|
||||||
|
text_rect = style.subElementRect(
|
||||||
|
QtWidgets.QCommonStyle.SE_ItemViewItemText,
|
||||||
|
option
|
||||||
|
)
|
||||||
|
text_margin = style.proxy().pixelMetric(
|
||||||
|
QtWidgets.QCommonStyle.PM_FocusFrameHMargin,
|
||||||
|
option,
|
||||||
|
option.widget
|
||||||
|
) + 1
|
||||||
|
padded_text_rect = text_rect.adjusted(
|
||||||
|
text_margin, 0, - text_margin, 0
|
||||||
|
)
|
||||||
|
|
||||||
|
fm = QtGui.QFontMetrics(option.font)
|
||||||
|
text = self._get_status_name(index)
|
||||||
|
if padded_text_rect.width() < fm.width(text):
|
||||||
|
text = self._get_status_short_name(index)
|
||||||
|
|
||||||
|
fg_color = self._get_status_color(index)
|
||||||
|
pen = painter.pen()
|
||||||
|
pen.setColor(fg_color)
|
||||||
|
painter.setPen(pen)
|
||||||
|
|
||||||
|
painter.drawText(
|
||||||
|
padded_text_rect,
|
||||||
|
option.displayAlignment,
|
||||||
|
text
|
||||||
|
)
|
||||||
|
|
||||||
|
painter.restore()
|
||||||
|
|
||||||
|
def _get_status_name(self, index):
|
||||||
|
return index.data(self.status_name_role)
|
||||||
|
|
||||||
|
def _get_status_short_name(self, index):
|
||||||
|
return index.data(self.status_short_name_role)
|
||||||
|
|
||||||
|
def _get_status_color(self, index):
|
||||||
|
return QtGui.QColor(index.data(self.status_color_role))
|
||||||
|
|
||||||
|
def _get_status_icon(self, index):
|
||||||
|
if self.status_icon_role is not None:
|
||||||
|
return index.data(self.status_icon_role)
|
||||||
|
return None
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import contextlib
|
import contextlib
|
||||||
|
import collections
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from qtpy import QtWidgets, QtCore, QtGui
|
from qtpy import QtWidgets, QtCore, QtGui
|
||||||
|
|
@ -196,16 +197,16 @@ def get_openpype_qt_app():
|
||||||
return get_ayon_qt_app()
|
return get_ayon_qt_app()
|
||||||
|
|
||||||
|
|
||||||
def iter_model_rows(model, column, include_root=False):
|
def iter_model_rows(model, column=0, include_root=False):
|
||||||
"""Iterate over all row indices in a model"""
|
"""Iterate over all row indices in a model"""
|
||||||
indices = [QtCore.QModelIndex()] # start iteration at root
|
indexes_queue = collections.deque()
|
||||||
|
# start iteration at root
|
||||||
for index in indices:
|
indexes_queue.append(QtCore.QModelIndex())
|
||||||
|
while indexes_queue:
|
||||||
|
index = indexes_queue.popleft()
|
||||||
# Add children to the iterations
|
# Add children to the iterations
|
||||||
child_rows = model.rowCount(index)
|
for child_row in range(model.rowCount(index)):
|
||||||
for child_row in range(child_rows):
|
indexes_queue.append(model.index(child_row, column, index))
|
||||||
child_index = model.index(child_row, column, index)
|
|
||||||
indices.append(child_index)
|
|
||||||
|
|
||||||
if not include_root and not index.isValid():
|
if not include_root and not index.isValid():
|
||||||
continue
|
continue
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,10 @@ class WorkfileInfo:
|
||||||
task_id (str): Task id.
|
task_id (str): Task id.
|
||||||
filepath (str): Filepath.
|
filepath (str): Filepath.
|
||||||
filesize (int): File size.
|
filesize (int): File size.
|
||||||
creation_time (int): Creation time (timestamp).
|
creation_time (float): Creation time (timestamp).
|
||||||
modification_time (int): Modification time (timestamp).
|
modification_time (float): Modification time (timestamp).
|
||||||
|
created_by (Union[str, none]): User who created the file.
|
||||||
|
updated_by (Union[str, none]): User who last updated the file.
|
||||||
note (str): Note.
|
note (str): Note.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
@ -26,6 +28,8 @@ class WorkfileInfo:
|
||||||
filesize,
|
filesize,
|
||||||
creation_time,
|
creation_time,
|
||||||
modification_time,
|
modification_time,
|
||||||
|
created_by,
|
||||||
|
updated_by,
|
||||||
note,
|
note,
|
||||||
):
|
):
|
||||||
self.folder_id = folder_id
|
self.folder_id = folder_id
|
||||||
|
|
@ -34,6 +38,8 @@ class WorkfileInfo:
|
||||||
self.filesize = filesize
|
self.filesize = filesize
|
||||||
self.creation_time = creation_time
|
self.creation_time = creation_time
|
||||||
self.modification_time = modification_time
|
self.modification_time = modification_time
|
||||||
|
self.created_by = created_by
|
||||||
|
self.updated_by = updated_by
|
||||||
self.note = note
|
self.note = note
|
||||||
|
|
||||||
def to_data(self):
|
def to_data(self):
|
||||||
|
|
@ -50,6 +56,8 @@ class WorkfileInfo:
|
||||||
"filesize": self.filesize,
|
"filesize": self.filesize,
|
||||||
"creation_time": self.creation_time,
|
"creation_time": self.creation_time,
|
||||||
"modification_time": self.modification_time,
|
"modification_time": self.modification_time,
|
||||||
|
"created_by": self.created_by,
|
||||||
|
"updated_by": self.updated_by,
|
||||||
"note": self.note,
|
"note": self.note,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -212,6 +220,7 @@ class FileItem:
|
||||||
dirpath (str): Directory path of file.
|
dirpath (str): Directory path of file.
|
||||||
filename (str): Filename.
|
filename (str): Filename.
|
||||||
modified (float): Modified timestamp.
|
modified (float): Modified timestamp.
|
||||||
|
created_by (Optional[str]): Username.
|
||||||
representation_id (Optional[str]): Representation id of published
|
representation_id (Optional[str]): Representation id of published
|
||||||
workfile.
|
workfile.
|
||||||
filepath (Optional[str]): Prepared filepath.
|
filepath (Optional[str]): Prepared filepath.
|
||||||
|
|
@ -223,6 +232,8 @@ class FileItem:
|
||||||
dirpath,
|
dirpath,
|
||||||
filename,
|
filename,
|
||||||
modified,
|
modified,
|
||||||
|
created_by=None,
|
||||||
|
updated_by=None,
|
||||||
representation_id=None,
|
representation_id=None,
|
||||||
filepath=None,
|
filepath=None,
|
||||||
exists=None
|
exists=None
|
||||||
|
|
@ -230,6 +241,8 @@ class FileItem:
|
||||||
self.filename = filename
|
self.filename = filename
|
||||||
self.dirpath = dirpath
|
self.dirpath = dirpath
|
||||||
self.modified = modified
|
self.modified = modified
|
||||||
|
self.created_by = created_by
|
||||||
|
self.updated_by = updated_by
|
||||||
self.representation_id = representation_id
|
self.representation_id = representation_id
|
||||||
self._filepath = filepath
|
self._filepath = filepath
|
||||||
self._exists = exists
|
self._exists = exists
|
||||||
|
|
@ -269,6 +282,7 @@ class FileItem:
|
||||||
"filename": self.filename,
|
"filename": self.filename,
|
||||||
"dirpath": self.dirpath,
|
"dirpath": self.dirpath,
|
||||||
"modified": self.modified,
|
"modified": self.modified,
|
||||||
|
"created_by": self.created_by,
|
||||||
"representation_id": self.representation_id,
|
"representation_id": self.representation_id,
|
||||||
"filepath": self.filepath,
|
"filepath": self.filepath,
|
||||||
"exists": self.exists,
|
"exists": self.exists,
|
||||||
|
|
@ -522,6 +536,16 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon):
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_user_items_by_name(self):
|
||||||
|
"""Get user items available on AYON server.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict[str, UserItem]: User items by username.
|
||||||
|
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
# Host information
|
# Host information
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_workfile_extensions(self):
|
def get_workfile_extensions(self):
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ from ayon_core.tools.common_models import (
|
||||||
HierarchyModel,
|
HierarchyModel,
|
||||||
HierarchyExpectedSelection,
|
HierarchyExpectedSelection,
|
||||||
ProjectsModel,
|
ProjectsModel,
|
||||||
|
UsersModel,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .abstract import (
|
from .abstract import (
|
||||||
|
|
@ -161,6 +162,7 @@ class BaseWorkfileController(
|
||||||
self._save_is_enabled = True
|
self._save_is_enabled = True
|
||||||
|
|
||||||
# Expected selected folder and task
|
# Expected selected folder and task
|
||||||
|
self._users_model = self._create_users_model()
|
||||||
self._expected_selection = self._create_expected_selection_obj()
|
self._expected_selection = self._create_expected_selection_obj()
|
||||||
self._selection_model = self._create_selection_model()
|
self._selection_model = self._create_selection_model()
|
||||||
self._projects_model = self._create_projects_model()
|
self._projects_model = self._create_projects_model()
|
||||||
|
|
@ -176,6 +178,12 @@ class BaseWorkfileController(
|
||||||
def is_host_valid(self):
|
def is_host_valid(self):
|
||||||
return self._host_is_valid
|
return self._host_is_valid
|
||||||
|
|
||||||
|
def _create_users_model(self):
|
||||||
|
return UsersModel(self)
|
||||||
|
|
||||||
|
def _create_workfiles_model(self):
|
||||||
|
return WorkfilesModel(self)
|
||||||
|
|
||||||
def _create_expected_selection_obj(self):
|
def _create_expected_selection_obj(self):
|
||||||
return WorkfilesToolExpectedSelection(self)
|
return WorkfilesToolExpectedSelection(self)
|
||||||
|
|
||||||
|
|
@ -188,9 +196,6 @@ class BaseWorkfileController(
|
||||||
def _create_hierarchy_model(self):
|
def _create_hierarchy_model(self):
|
||||||
return HierarchyModel(self)
|
return HierarchyModel(self)
|
||||||
|
|
||||||
def _create_workfiles_model(self):
|
|
||||||
return WorkfilesModel(self)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def event_system(self):
|
def event_system(self):
|
||||||
"""Inner event system for workfiles tool controller.
|
"""Inner event system for workfiles tool controller.
|
||||||
|
|
@ -272,6 +277,9 @@ class BaseWorkfileController(
|
||||||
{"enabled": enabled}
|
{"enabled": enabled}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_user_items_by_name(self):
|
||||||
|
return self._users_model.get_user_items_by_name()
|
||||||
|
|
||||||
# Host information
|
# Host information
|
||||||
def get_workfile_extensions(self):
|
def get_workfile_extensions(self):
|
||||||
host = self._host
|
host = self._host
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import arrow
|
||||||
import ayon_api
|
import ayon_api
|
||||||
from ayon_api.operations import OperationsSession
|
from ayon_api.operations import OperationsSession
|
||||||
|
|
||||||
|
from ayon_core.lib import get_ayon_username
|
||||||
from ayon_core.pipeline.template_data import (
|
from ayon_core.pipeline.template_data import (
|
||||||
get_template_data,
|
get_template_data,
|
||||||
get_task_template_data,
|
get_task_template_data,
|
||||||
|
|
@ -23,6 +24,8 @@ from ayon_core.tools.workfiles.abstract import (
|
||||||
WorkfileInfo,
|
WorkfileInfo,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_NOT_SET = object()
|
||||||
|
|
||||||
|
|
||||||
class CommentMatcher(object):
|
class CommentMatcher(object):
|
||||||
"""Use anatomy and work file data to parse comments from filenames.
|
"""Use anatomy and work file data to parse comments from filenames.
|
||||||
|
|
@ -188,10 +191,17 @@ class WorkareaModel:
|
||||||
if ext not in self._extensions:
|
if ext not in self._extensions:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
modified = os.path.getmtime(filepath)
|
workfile_info = self._controller.get_workfile_info(
|
||||||
items.append(
|
folder_id, task_id, filepath
|
||||||
FileItem(workdir, filename, modified)
|
|
||||||
)
|
)
|
||||||
|
modified = os.path.getmtime(filepath)
|
||||||
|
items.append(FileItem(
|
||||||
|
workdir,
|
||||||
|
filename,
|
||||||
|
modified,
|
||||||
|
workfile_info.created_by,
|
||||||
|
workfile_info.updated_by,
|
||||||
|
))
|
||||||
return items
|
return items
|
||||||
|
|
||||||
def _get_template_key(self, fill_data):
|
def _get_template_key(self, fill_data):
|
||||||
|
|
@ -439,6 +449,7 @@ class WorkfileEntitiesModel:
|
||||||
self._controller = controller
|
self._controller = controller
|
||||||
self._cache = {}
|
self._cache = {}
|
||||||
self._items = {}
|
self._items = {}
|
||||||
|
self._current_username = _NOT_SET
|
||||||
|
|
||||||
def _get_workfile_info_identifier(
|
def _get_workfile_info_identifier(
|
||||||
self, folder_id, task_id, rootless_path
|
self, folder_id, task_id, rootless_path
|
||||||
|
|
@ -459,8 +470,12 @@ class WorkfileEntitiesModel:
|
||||||
self, folder_id, task_id, workfile_info, filepath
|
self, folder_id, task_id, workfile_info, filepath
|
||||||
):
|
):
|
||||||
note = ""
|
note = ""
|
||||||
|
created_by = None
|
||||||
|
updated_by = None
|
||||||
if workfile_info:
|
if workfile_info:
|
||||||
note = workfile_info["attrib"].get("description") or ""
|
note = workfile_info["attrib"].get("description") or ""
|
||||||
|
created_by = workfile_info.get("createdBy")
|
||||||
|
updated_by = workfile_info.get("updatedBy")
|
||||||
|
|
||||||
filestat = os.stat(filepath)
|
filestat = os.stat(filepath)
|
||||||
return WorkfileInfo(
|
return WorkfileInfo(
|
||||||
|
|
@ -470,6 +485,8 @@ class WorkfileEntitiesModel:
|
||||||
filesize=filestat.st_size,
|
filesize=filestat.st_size,
|
||||||
creation_time=filestat.st_ctime,
|
creation_time=filestat.st_ctime,
|
||||||
modification_time=filestat.st_mtime,
|
modification_time=filestat.st_mtime,
|
||||||
|
created_by=created_by,
|
||||||
|
updated_by=updated_by,
|
||||||
note=note
|
note=note
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -481,7 +498,7 @@ class WorkfileEntitiesModel:
|
||||||
for workfile_info in ayon_api.get_workfiles_info(
|
for workfile_info in ayon_api.get_workfiles_info(
|
||||||
self._controller.get_current_project_name(),
|
self._controller.get_current_project_name(),
|
||||||
task_ids=[task_id],
|
task_ids=[task_id],
|
||||||
fields=["id", "path", "attrib"],
|
fields=["id", "path", "attrib", "createdBy", "updatedBy"],
|
||||||
):
|
):
|
||||||
workfile_identifier = self._get_workfile_info_identifier(
|
workfile_identifier = self._get_workfile_info_identifier(
|
||||||
folder_id, task_id, workfile_info["path"]
|
folder_id, task_id, workfile_info["path"]
|
||||||
|
|
@ -525,18 +542,32 @@ class WorkfileEntitiesModel:
|
||||||
self._items.pop(identifier, None)
|
self._items.pop(identifier, None)
|
||||||
return
|
return
|
||||||
|
|
||||||
if note is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
old_note = workfile_info.get("attrib", {}).get("note")
|
old_note = workfile_info.get("attrib", {}).get("note")
|
||||||
|
|
||||||
new_workfile_info = copy.deepcopy(workfile_info)
|
new_workfile_info = copy.deepcopy(workfile_info)
|
||||||
attrib = new_workfile_info.setdefault("attrib", {})
|
update_data = {}
|
||||||
attrib["description"] = note
|
if note is not None and old_note != note:
|
||||||
|
update_data["attrib"] = {"description": note}
|
||||||
|
attrib = new_workfile_info.setdefault("attrib", {})
|
||||||
|
attrib["description"] = note
|
||||||
|
|
||||||
|
username = self._get_current_username()
|
||||||
|
# Automatically fix 'createdBy' and 'updatedBy' fields
|
||||||
|
# NOTE both fields were not automatically filled by server
|
||||||
|
# until 1.1.3 release.
|
||||||
|
if workfile_info.get("createdBy") is None:
|
||||||
|
update_data["createdBy"] = username
|
||||||
|
new_workfile_info["createdBy"] = username
|
||||||
|
|
||||||
|
if workfile_info.get("updatedBy") != username:
|
||||||
|
update_data["updatedBy"] = username
|
||||||
|
new_workfile_info["updatedBy"] = username
|
||||||
|
|
||||||
|
if not update_data:
|
||||||
|
return
|
||||||
|
|
||||||
self._cache[identifier] = new_workfile_info
|
self._cache[identifier] = new_workfile_info
|
||||||
self._items.pop(identifier, None)
|
self._items.pop(identifier, None)
|
||||||
if old_note == note:
|
|
||||||
return
|
|
||||||
|
|
||||||
project_name = self._controller.get_current_project_name()
|
project_name = self._controller.get_current_project_name()
|
||||||
|
|
||||||
|
|
@ -545,7 +576,7 @@ class WorkfileEntitiesModel:
|
||||||
project_name,
|
project_name,
|
||||||
"workfile",
|
"workfile",
|
||||||
workfile_info["id"],
|
workfile_info["id"],
|
||||||
{"attrib": {"description": note}},
|
update_data,
|
||||||
)
|
)
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
|
|
@ -554,13 +585,18 @@ class WorkfileEntitiesModel:
|
||||||
|
|
||||||
project_name = self._controller.get_current_project_name()
|
project_name = self._controller.get_current_project_name()
|
||||||
|
|
||||||
|
username = self._get_current_username()
|
||||||
workfile_info = {
|
workfile_info = {
|
||||||
"path": rootless_path,
|
"path": rootless_path,
|
||||||
"taskId": task_id,
|
"taskId": task_id,
|
||||||
"attrib": {
|
"attrib": {
|
||||||
"extension": extension,
|
"extension": extension,
|
||||||
"description": note
|
"description": note
|
||||||
}
|
},
|
||||||
|
# TODO remove 'createdBy' and 'updatedBy' fields when server is
|
||||||
|
# or above 1.1.3 .
|
||||||
|
"createdBy": username,
|
||||||
|
"updatedBy": username,
|
||||||
}
|
}
|
||||||
|
|
||||||
session = OperationsSession()
|
session = OperationsSession()
|
||||||
|
|
@ -568,6 +604,11 @@ class WorkfileEntitiesModel:
|
||||||
session.commit()
|
session.commit()
|
||||||
return workfile_info
|
return workfile_info
|
||||||
|
|
||||||
|
def _get_current_username(self):
|
||||||
|
if self._current_username is _NOT_SET:
|
||||||
|
self._current_username = get_ayon_username()
|
||||||
|
return self._current_username
|
||||||
|
|
||||||
|
|
||||||
class PublishWorkfilesModel:
|
class PublishWorkfilesModel:
|
||||||
"""Model for handling of published workfiles.
|
"""Model for handling of published workfiles.
|
||||||
|
|
@ -599,7 +640,7 @@ class PublishWorkfilesModel:
|
||||||
return self._cached_repre_extensions
|
return self._cached_repre_extensions
|
||||||
|
|
||||||
def _file_item_from_representation(
|
def _file_item_from_representation(
|
||||||
self, repre_entity, project_anatomy, task_name=None
|
self, repre_entity, project_anatomy, author, task_name=None
|
||||||
):
|
):
|
||||||
if task_name is not None:
|
if task_name is not None:
|
||||||
task_info = repre_entity["context"].get("task")
|
task_info = repre_entity["context"].get("task")
|
||||||
|
|
@ -634,6 +675,8 @@ class PublishWorkfilesModel:
|
||||||
dirpath,
|
dirpath,
|
||||||
filename,
|
filename,
|
||||||
created_at.float_timestamp,
|
created_at.float_timestamp,
|
||||||
|
author,
|
||||||
|
None,
|
||||||
repre_entity["id"]
|
repre_entity["id"]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -643,9 +686,9 @@ class PublishWorkfilesModel:
|
||||||
# Get subset docs of folder
|
# Get subset docs of folder
|
||||||
product_entities = ayon_api.get_products(
|
product_entities = ayon_api.get_products(
|
||||||
project_name,
|
project_name,
|
||||||
folder_ids=[folder_id],
|
folder_ids={folder_id},
|
||||||
product_types=["workfile"],
|
product_types={"workfile"},
|
||||||
fields=["id", "name"]
|
fields={"id", "name"}
|
||||||
)
|
)
|
||||||
|
|
||||||
output = []
|
output = []
|
||||||
|
|
@ -657,25 +700,33 @@ class PublishWorkfilesModel:
|
||||||
version_entities = ayon_api.get_versions(
|
version_entities = ayon_api.get_versions(
|
||||||
project_name,
|
project_name,
|
||||||
product_ids=product_ids,
|
product_ids=product_ids,
|
||||||
fields=["id", "productId"]
|
fields={"id", "author"}
|
||||||
)
|
)
|
||||||
version_ids = {version["id"] for version in version_entities}
|
versions_by_id = {
|
||||||
if not version_ids:
|
version["id"]: version
|
||||||
|
for version in version_entities
|
||||||
|
}
|
||||||
|
if not versions_by_id:
|
||||||
return output
|
return output
|
||||||
|
|
||||||
# Query representations of filtered versions and add filter for
|
# Query representations of filtered versions and add filter for
|
||||||
# extension
|
# extension
|
||||||
repre_entities = ayon_api.get_representations(
|
repre_entities = ayon_api.get_representations(
|
||||||
project_name,
|
project_name,
|
||||||
version_ids=version_ids
|
version_ids=set(versions_by_id)
|
||||||
)
|
)
|
||||||
project_anatomy = self._controller.project_anatomy
|
project_anatomy = self._controller.project_anatomy
|
||||||
|
|
||||||
# Filter queried representations by task name if task is set
|
# Filter queried representations by task name if task is set
|
||||||
file_items = []
|
file_items = []
|
||||||
for repre_entity in repre_entities:
|
for repre_entity in repre_entities:
|
||||||
|
version_id = repre_entity["versionId"]
|
||||||
|
version_entity = versions_by_id[version_id]
|
||||||
file_item = self._file_item_from_representation(
|
file_item = self._file_item_from_representation(
|
||||||
repre_entity, project_anatomy, task_name
|
repre_entity,
|
||||||
|
project_anatomy,
|
||||||
|
version_entity["author"],
|
||||||
|
task_name,
|
||||||
)
|
)
|
||||||
if file_item is not None:
|
if file_item is not None:
|
||||||
file_items.append(file_item)
|
file_items.append(file_item)
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,8 @@ from .utils import BaseOverlayFrame
|
||||||
|
|
||||||
REPRE_ID_ROLE = QtCore.Qt.UserRole + 1
|
REPRE_ID_ROLE = QtCore.Qt.UserRole + 1
|
||||||
FILEPATH_ROLE = QtCore.Qt.UserRole + 2
|
FILEPATH_ROLE = QtCore.Qt.UserRole + 2
|
||||||
DATE_MODIFIED_ROLE = QtCore.Qt.UserRole + 3
|
AUTHOR_ROLE = QtCore.Qt.UserRole + 3
|
||||||
|
DATE_MODIFIED_ROLE = QtCore.Qt.UserRole + 4
|
||||||
|
|
||||||
|
|
||||||
class PublishedFilesModel(QtGui.QStandardItemModel):
|
class PublishedFilesModel(QtGui.QStandardItemModel):
|
||||||
|
|
@ -23,13 +24,19 @@ class PublishedFilesModel(QtGui.QStandardItemModel):
|
||||||
controller (AbstractWorkfilesFrontend): The control object.
|
controller (AbstractWorkfilesFrontend): The control object.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
columns = [
|
||||||
|
"Name",
|
||||||
|
"Author",
|
||||||
|
"Date Modified",
|
||||||
|
]
|
||||||
|
date_modified_col = columns.index("Date Modified")
|
||||||
|
|
||||||
def __init__(self, controller):
|
def __init__(self, controller):
|
||||||
super(PublishedFilesModel, self).__init__()
|
super(PublishedFilesModel, self).__init__()
|
||||||
|
|
||||||
self.setColumnCount(2)
|
self.setColumnCount(len(self.columns))
|
||||||
|
for idx, label in enumerate(self.columns):
|
||||||
self.setHeaderData(0, QtCore.Qt.Horizontal, "Name")
|
self.setHeaderData(idx, QtCore.Qt.Horizontal, label)
|
||||||
self.setHeaderData(1, QtCore.Qt.Horizontal, "Date Modified")
|
|
||||||
|
|
||||||
controller.register_event_callback(
|
controller.register_event_callback(
|
||||||
"selection.task.changed",
|
"selection.task.changed",
|
||||||
|
|
@ -185,6 +192,8 @@ class PublishedFilesModel(QtGui.QStandardItemModel):
|
||||||
self._remove_empty_item()
|
self._remove_empty_item()
|
||||||
self._remove_missing_context_item()
|
self._remove_missing_context_item()
|
||||||
|
|
||||||
|
user_items_by_name = self._controller.get_user_items_by_name()
|
||||||
|
|
||||||
items_to_remove = set(self._items_by_id.keys())
|
items_to_remove = set(self._items_by_id.keys())
|
||||||
new_items = []
|
new_items = []
|
||||||
for file_item in file_items:
|
for file_item in file_items:
|
||||||
|
|
@ -205,8 +214,15 @@ class PublishedFilesModel(QtGui.QStandardItemModel):
|
||||||
else:
|
else:
|
||||||
flags = QtCore.Qt.NoItemFlags
|
flags = QtCore.Qt.NoItemFlags
|
||||||
|
|
||||||
|
author = file_item.created_by
|
||||||
|
user_item = user_items_by_name.get(author)
|
||||||
|
if user_item is not None and user_item.full_name:
|
||||||
|
author = user_item.full_name
|
||||||
|
|
||||||
item.setFlags(flags)
|
item.setFlags(flags)
|
||||||
|
|
||||||
item.setData(file_item.filepath, FILEPATH_ROLE)
|
item.setData(file_item.filepath, FILEPATH_ROLE)
|
||||||
|
item.setData(author, AUTHOR_ROLE)
|
||||||
item.setData(file_item.modified, DATE_MODIFIED_ROLE)
|
item.setData(file_item.modified, DATE_MODIFIED_ROLE)
|
||||||
|
|
||||||
self._items_by_id[repre_id] = item
|
self._items_by_id[repre_id] = item
|
||||||
|
|
@ -225,22 +241,30 @@ class PublishedFilesModel(QtGui.QStandardItemModel):
|
||||||
# Use flags of first column for all columns
|
# Use flags of first column for all columns
|
||||||
if index.column() != 0:
|
if index.column() != 0:
|
||||||
index = self.index(index.row(), 0, index.parent())
|
index = self.index(index.row(), 0, index.parent())
|
||||||
return super(PublishedFilesModel, self).flags(index)
|
return super().flags(index)
|
||||||
|
|
||||||
def data(self, index, role=None):
|
def data(self, index, role=None):
|
||||||
if role is None:
|
if role is None:
|
||||||
role = QtCore.Qt.DisplayRole
|
role = QtCore.Qt.DisplayRole
|
||||||
|
|
||||||
# Handle roles for first column
|
# Handle roles for first column
|
||||||
if index.column() == 1:
|
col = index.column()
|
||||||
if role == QtCore.Qt.DecorationRole:
|
if col != 1:
|
||||||
return None
|
return super().data(index, role)
|
||||||
|
|
||||||
if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):
|
if role == QtCore.Qt.DecorationRole:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):
|
||||||
|
if col == 1:
|
||||||
|
role = AUTHOR_ROLE
|
||||||
|
elif col == 2:
|
||||||
role = DATE_MODIFIED_ROLE
|
role = DATE_MODIFIED_ROLE
|
||||||
index = self.index(index.row(), 0, index.parent())
|
else:
|
||||||
|
return None
|
||||||
|
index = self.index(index.row(), 0, index.parent())
|
||||||
|
|
||||||
return super(PublishedFilesModel, self).data(index, role)
|
return super().data(index, role)
|
||||||
|
|
||||||
|
|
||||||
class SelectContextOverlay(BaseOverlayFrame):
|
class SelectContextOverlay(BaseOverlayFrame):
|
||||||
|
|
@ -295,7 +319,7 @@ class PublishedFilesWidget(QtWidgets.QWidget):
|
||||||
view.setModel(proxy_model)
|
view.setModel(proxy_model)
|
||||||
|
|
||||||
time_delegate = PrettyTimeDelegate()
|
time_delegate = PrettyTimeDelegate()
|
||||||
view.setItemDelegateForColumn(1, time_delegate)
|
view.setItemDelegateForColumn(model.date_modified_col, time_delegate)
|
||||||
|
|
||||||
# Default to a wider first filename column it is what we mostly care
|
# Default to a wider first filename column it is what we mostly care
|
||||||
# about and the date modified is relatively small anyway.
|
# about and the date modified is relatively small anyway.
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,8 @@ from ayon_core.tools.utils.delegates import PrettyTimeDelegate
|
||||||
|
|
||||||
FILENAME_ROLE = QtCore.Qt.UserRole + 1
|
FILENAME_ROLE = QtCore.Qt.UserRole + 1
|
||||||
FILEPATH_ROLE = QtCore.Qt.UserRole + 2
|
FILEPATH_ROLE = QtCore.Qt.UserRole + 2
|
||||||
DATE_MODIFIED_ROLE = QtCore.Qt.UserRole + 3
|
AUTHOR_ROLE = QtCore.Qt.UserRole + 3
|
||||||
|
DATE_MODIFIED_ROLE = QtCore.Qt.UserRole + 4
|
||||||
|
|
||||||
|
|
||||||
class WorkAreaFilesModel(QtGui.QStandardItemModel):
|
class WorkAreaFilesModel(QtGui.QStandardItemModel):
|
||||||
|
|
@ -21,14 +22,20 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
refreshed = QtCore.Signal()
|
refreshed = QtCore.Signal()
|
||||||
|
columns = [
|
||||||
|
"Name",
|
||||||
|
"Author",
|
||||||
|
"Date Modified",
|
||||||
|
]
|
||||||
|
date_modified_col = columns.index("Date Modified")
|
||||||
|
|
||||||
def __init__(self, controller):
|
def __init__(self, controller):
|
||||||
super(WorkAreaFilesModel, self).__init__()
|
super(WorkAreaFilesModel, self).__init__()
|
||||||
|
|
||||||
self.setColumnCount(2)
|
self.setColumnCount(len(self.columns))
|
||||||
|
|
||||||
self.setHeaderData(0, QtCore.Qt.Horizontal, "Name")
|
for idx, label in enumerate(self.columns):
|
||||||
self.setHeaderData(1, QtCore.Qt.Horizontal, "Date Modified")
|
self.setHeaderData(idx, QtCore.Qt.Horizontal, label)
|
||||||
|
|
||||||
controller.register_event_callback(
|
controller.register_event_callback(
|
||||||
"selection.folder.changed",
|
"selection.folder.changed",
|
||||||
|
|
@ -186,6 +193,7 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel):
|
||||||
return
|
return
|
||||||
self._remove_empty_item()
|
self._remove_empty_item()
|
||||||
self._remove_missing_context_item()
|
self._remove_missing_context_item()
|
||||||
|
user_items_by_name = self._controller.get_user_items_by_name()
|
||||||
|
|
||||||
items_to_remove = set(self._items_by_filename.keys())
|
items_to_remove = set(self._items_by_filename.keys())
|
||||||
new_items = []
|
new_items = []
|
||||||
|
|
@ -205,7 +213,13 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel):
|
||||||
item.setData(file_item.filename, QtCore.Qt.DisplayRole)
|
item.setData(file_item.filename, QtCore.Qt.DisplayRole)
|
||||||
item.setData(file_item.filename, FILENAME_ROLE)
|
item.setData(file_item.filename, FILENAME_ROLE)
|
||||||
|
|
||||||
|
updated_by = file_item.updated_by
|
||||||
|
user_item = user_items_by_name.get(updated_by)
|
||||||
|
if user_item is not None and user_item.full_name:
|
||||||
|
updated_by = user_item.full_name
|
||||||
|
|
||||||
item.setData(file_item.filepath, FILEPATH_ROLE)
|
item.setData(file_item.filepath, FILEPATH_ROLE)
|
||||||
|
item.setData(updated_by, AUTHOR_ROLE)
|
||||||
item.setData(file_item.modified, DATE_MODIFIED_ROLE)
|
item.setData(file_item.modified, DATE_MODIFIED_ROLE)
|
||||||
|
|
||||||
self._items_by_filename[file_item.filename] = item
|
self._items_by_filename[file_item.filename] = item
|
||||||
|
|
@ -224,22 +238,30 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel):
|
||||||
# Use flags of first column for all columns
|
# Use flags of first column for all columns
|
||||||
if index.column() != 0:
|
if index.column() != 0:
|
||||||
index = self.index(index.row(), 0, index.parent())
|
index = self.index(index.row(), 0, index.parent())
|
||||||
return super(WorkAreaFilesModel, self).flags(index)
|
return super().flags(index)
|
||||||
|
|
||||||
def data(self, index, role=None):
|
def data(self, index, role=None):
|
||||||
if role is None:
|
if role is None:
|
||||||
role = QtCore.Qt.DisplayRole
|
role = QtCore.Qt.DisplayRole
|
||||||
|
|
||||||
# Handle roles for first column
|
# Handle roles for first column
|
||||||
if index.column() == 1:
|
col = index.column()
|
||||||
if role == QtCore.Qt.DecorationRole:
|
if col == 0:
|
||||||
return None
|
return super().data(index, role)
|
||||||
|
|
||||||
if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):
|
if role == QtCore.Qt.DecorationRole:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):
|
||||||
|
if col == 1:
|
||||||
|
role = AUTHOR_ROLE
|
||||||
|
elif col == 2:
|
||||||
role = DATE_MODIFIED_ROLE
|
role = DATE_MODIFIED_ROLE
|
||||||
index = self.index(index.row(), 0, index.parent())
|
else:
|
||||||
|
return None
|
||||||
|
index = self.index(index.row(), 0, index.parent())
|
||||||
|
|
||||||
return super(WorkAreaFilesModel, self).data(index, role)
|
return super().data(index, role)
|
||||||
|
|
||||||
def set_published_mode(self, published_mode):
|
def set_published_mode(self, published_mode):
|
||||||
if self._published_mode == published_mode:
|
if self._published_mode == published_mode:
|
||||||
|
|
@ -279,7 +301,7 @@ class WorkAreaFilesWidget(QtWidgets.QWidget):
|
||||||
view.setModel(proxy_model)
|
view.setModel(proxy_model)
|
||||||
|
|
||||||
time_delegate = PrettyTimeDelegate()
|
time_delegate = PrettyTimeDelegate()
|
||||||
view.setItemDelegateForColumn(1, time_delegate)
|
view.setItemDelegateForColumn(model.date_modified_col, time_delegate)
|
||||||
|
|
||||||
# Default to a wider first filename column it is what we mostly care
|
# Default to a wider first filename column it is what we mostly care
|
||||||
# about and the date modified is relatively small anyway.
|
# about and the date modified is relatively small anyway.
|
||||||
|
|
|
||||||
|
|
@ -147,13 +147,38 @@ class SidePanelWidget(QtWidgets.QWidget):
|
||||||
workfile_info.creation_time)
|
workfile_info.creation_time)
|
||||||
modification_time = datetime.datetime.fromtimestamp(
|
modification_time = datetime.datetime.fromtimestamp(
|
||||||
workfile_info.modification_time)
|
workfile_info.modification_time)
|
||||||
|
|
||||||
|
user_items_by_name = self._controller.get_user_items_by_name()
|
||||||
|
|
||||||
|
def convert_username(username):
|
||||||
|
user_item = user_items_by_name.get(username)
|
||||||
|
if user_item is not None and user_item.full_name:
|
||||||
|
return user_item.full_name
|
||||||
|
return username
|
||||||
|
|
||||||
|
created_lines = [
|
||||||
|
creation_time.strftime(datetime_format)
|
||||||
|
]
|
||||||
|
if workfile_info.created_by:
|
||||||
|
created_lines.insert(
|
||||||
|
0, convert_username(workfile_info.created_by)
|
||||||
|
)
|
||||||
|
|
||||||
|
modified_lines = [
|
||||||
|
modification_time.strftime(datetime_format)
|
||||||
|
]
|
||||||
|
if workfile_info.updated_by:
|
||||||
|
modified_lines.insert(
|
||||||
|
0, convert_username(workfile_info.updated_by)
|
||||||
|
)
|
||||||
|
|
||||||
lines = (
|
lines = (
|
||||||
"<b>Size:</b>",
|
"<b>Size:</b>",
|
||||||
size_value,
|
size_value,
|
||||||
"<b>Created:</b>",
|
"<b>Created:</b>",
|
||||||
creation_time.strftime(datetime_format),
|
"<br/>".join(created_lines),
|
||||||
"<b>Modified:</b>",
|
"<b>Modified:</b>",
|
||||||
modification_time.strftime(datetime_format)
|
"<br/>".join(modified_lines),
|
||||||
)
|
)
|
||||||
self._orig_note = note
|
self._orig_note = note
|
||||||
self._note_input.setPlainText(note)
|
self._note_input.setPlainText(note)
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,7 @@ class WorkfilesToolWindow(QtWidgets.QWidget):
|
||||||
split_widget.addWidget(tasks_widget)
|
split_widget.addWidget(tasks_widget)
|
||||||
split_widget.addWidget(col_3_widget)
|
split_widget.addWidget(col_3_widget)
|
||||||
split_widget.addWidget(side_panel)
|
split_widget.addWidget(side_panel)
|
||||||
split_widget.setSizes([255, 160, 455, 175])
|
split_widget.setSizes([255, 175, 550, 190])
|
||||||
|
|
||||||
body_layout.addWidget(split_widget)
|
body_layout.addWidget(split_widget)
|
||||||
|
|
||||||
|
|
@ -169,7 +169,7 @@ class WorkfilesToolWindow(QtWidgets.QWidget):
|
||||||
# Force focus on the open button by default, required for Houdini.
|
# Force focus on the open button by default, required for Houdini.
|
||||||
self._files_widget.setFocus()
|
self._files_widget.setFocus()
|
||||||
|
|
||||||
self.resize(1200, 600)
|
self.resize(1260, 600)
|
||||||
|
|
||||||
def _create_col_1_widget(self, controller, parent):
|
def _create_col_1_widget(self, controller, parent):
|
||||||
col_widget = QtWidgets.QWidget(parent)
|
col_widget = QtWidgets.QWidget(parent)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""Package declaring AYON core addon version."""
|
"""Package declaring AYON core addon version."""
|
||||||
__version__ = "0.3.1"
|
__version__ = "0.3.3-dev.1"
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ aiohttp_json_rpc = "*" # TVPaint server
|
||||||
aiohttp-middlewares = "^2.0.0"
|
aiohttp-middlewares = "^2.0.0"
|
||||||
wsrpc_aiohttp = "^3.1.1" # websocket server
|
wsrpc_aiohttp = "^3.1.1" # websocket server
|
||||||
Click = "^8"
|
Click = "^8"
|
||||||
OpenTimelineIO = "0.14.1"
|
OpenTimelineIO = "0.16.0"
|
||||||
opencolorio = "2.2.1"
|
opencolorio = "2.2.1"
|
||||||
Pillow = "9.5.0"
|
Pillow = "9.5.0"
|
||||||
pynput = "^1.7.2" # Timers manager - TODO remove
|
pynput = "^1.7.2" # Timers manager - TODO remove
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
name = "core"
|
name = "core"
|
||||||
title = "Core"
|
title = "Core"
|
||||||
version = "0.3.1"
|
version = "0.3.3-dev.1"
|
||||||
|
|
||||||
client_dir = "ayon_core"
|
client_dir = "ayon_core"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -80,11 +80,11 @@ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
||||||
exclude = [
|
exclude = [
|
||||||
"client/ayon_core/hosts/unreal/integration/*",
|
"client/ayon_core/hosts/unreal/integration/*",
|
||||||
"client/ayon_core/hosts/aftereffects/api/extension/js/libs/*",
|
"client/ayon_core/hosts/aftereffects/api/extension/js/libs/*",
|
||||||
"client/ayon_core/hosts/hiero/api/startup/*",
|
|
||||||
"client/ayon_core/modules/deadline/repository/custom/plugins/CelAction/*",
|
"client/ayon_core/modules/deadline/repository/custom/plugins/CelAction/*",
|
||||||
"client/ayon_core/modules/deadline/repository/custom/plugins/HarmonyAYON/*",
|
"client/ayon_core/modules/deadline/repository/custom/plugins/HarmonyAYON/*",
|
||||||
"client/ayon_core/modules/click_wrap.py",
|
"client/ayon_core/modules/click_wrap.py",
|
||||||
"client/ayon_core/scripts/slates/__init__.py"
|
"client/ayon_core/scripts/slates/__init__.py",
|
||||||
|
"server_addon/hiero/client/ayon_hiero/api/startup/*"
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.ruff.lint.per-file-ignores]
|
[tool.ruff.lint.per-file-ignores]
|
||||||
|
|
@ -108,6 +108,10 @@ line-ending = "auto"
|
||||||
# Ignore words that are not in the dictionary.
|
# Ignore words that are not in the dictionary.
|
||||||
ignore-words-list = "ayon,ynput,parms,parm,hda,developpement,ue"
|
ignore-words-list = "ayon,ynput,parms,parm,hda,developpement,ue"
|
||||||
|
|
||||||
|
# Ignore lines that contain this regex. This is hack for missing inline ignore.
|
||||||
|
# Remove with next codespell release (>2.2.6)
|
||||||
|
ignore-regex = ".*codespell:ignore.*"
|
||||||
|
|
||||||
skip = "./.*,./package/*,*/vendor/*,*/unreal/integration/*,*/aftereffects/api/extension/js/libs/*"
|
skip = "./.*,./package/*,*/vendor/*,*/unreal/integration/*,*/aftereffects/api/extension/js/libs/*"
|
||||||
count = true
|
count = true
|
||||||
quiet-level = 3
|
quiet-level = 3
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,12 @@
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from ayon_server.addons import BaseServerAddon
|
from ayon_server.addons import BaseServerAddon
|
||||||
|
|
||||||
from .settings import CoreSettings, DEFAULT_VALUES
|
from .settings import (
|
||||||
|
CoreSettings,
|
||||||
|
DEFAULT_VALUES,
|
||||||
|
convert_settings_overrides,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CoreAddon(BaseServerAddon):
|
class CoreAddon(BaseServerAddon):
|
||||||
|
|
@ -9,3 +15,14 @@ class CoreAddon(BaseServerAddon):
|
||||||
async def get_default_settings(self):
|
async def get_default_settings(self):
|
||||||
settings_model_cls = self.get_settings_model()
|
settings_model_cls = self.get_settings_model()
|
||||||
return settings_model_cls(**DEFAULT_VALUES)
|
return settings_model_cls(**DEFAULT_VALUES)
|
||||||
|
|
||||||
|
async def convert_settings_overrides(
|
||||||
|
self,
|
||||||
|
source_version: str,
|
||||||
|
overrides: dict[str, Any],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
convert_settings_overrides(source_version, overrides)
|
||||||
|
# Use super conversion
|
||||||
|
return await super().convert_settings_overrides(
|
||||||
|
source_version, overrides
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
from .main import CoreSettings, DEFAULT_VALUES
|
from .main import CoreSettings, DEFAULT_VALUES
|
||||||
|
from .conversion import convert_settings_overrides
|
||||||
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
"CoreSettings",
|
"CoreSettings",
|
||||||
"DEFAULT_VALUES",
|
"DEFAULT_VALUES",
|
||||||
|
|
||||||
|
"convert_settings_overrides",
|
||||||
)
|
)
|
||||||
|
|
|
||||||
86
server/settings/conversion.py
Normal file
86
server/settings/conversion.py
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
import copy
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from .publish_plugins import DEFAULT_PUBLISH_VALUES
|
||||||
|
|
||||||
|
|
||||||
|
def _convert_imageio_configs_0_3_1(overrides):
|
||||||
|
"""Imageio config settings did change to profiles since 0.3.1. ."""
|
||||||
|
imageio_overrides = overrides.get("imageio") or {}
|
||||||
|
if (
|
||||||
|
"ocio_config" not in imageio_overrides
|
||||||
|
or "filepath" not in imageio_overrides["ocio_config"]
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
ocio_config = imageio_overrides.pop("ocio_config")
|
||||||
|
|
||||||
|
filepath = ocio_config["filepath"]
|
||||||
|
if not filepath:
|
||||||
|
return
|
||||||
|
first_filepath = filepath[0]
|
||||||
|
ocio_config_profiles = imageio_overrides.setdefault(
|
||||||
|
"ocio_config_profiles", []
|
||||||
|
)
|
||||||
|
base_value = {
|
||||||
|
"type": "builtin_path",
|
||||||
|
"product_name": "",
|
||||||
|
"host_names": [],
|
||||||
|
"task_names": [],
|
||||||
|
"task_types": [],
|
||||||
|
"custom_path": "",
|
||||||
|
"builtin_path": "{BUILTIN_OCIO_ROOT}/aces_1.2/config.ocio"
|
||||||
|
}
|
||||||
|
if first_filepath in (
|
||||||
|
"{BUILTIN_OCIO_ROOT}/aces_1.2/config.ocio",
|
||||||
|
"{BUILTIN_OCIO_ROOT}/nuke-default/config.ocio",
|
||||||
|
):
|
||||||
|
base_value["type"] = "builtin_path"
|
||||||
|
base_value["builtin_path"] = first_filepath
|
||||||
|
else:
|
||||||
|
base_value["type"] = "custom_path"
|
||||||
|
base_value["custom_path"] = first_filepath
|
||||||
|
|
||||||
|
ocio_config_profiles.append(base_value)
|
||||||
|
|
||||||
|
|
||||||
|
def _convert_validate_version_0_3_3(publish_overrides):
|
||||||
|
"""ValidateVersion plugin changed in 0.3.3."""
|
||||||
|
if "ValidateVersion" not in publish_overrides:
|
||||||
|
return
|
||||||
|
|
||||||
|
validate_version = publish_overrides["ValidateVersion"]
|
||||||
|
# Already new settings
|
||||||
|
if "plugin_state_profiles" in validate_version:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Use new default profile as base
|
||||||
|
profile = copy.deepcopy(
|
||||||
|
DEFAULT_PUBLISH_VALUES["ValidateVersion"]["plugin_state_profiles"][0]
|
||||||
|
)
|
||||||
|
# Copy values from old overrides to new overrides
|
||||||
|
for key in {
|
||||||
|
"enabled",
|
||||||
|
"optional",
|
||||||
|
"active",
|
||||||
|
}:
|
||||||
|
if key not in validate_version:
|
||||||
|
continue
|
||||||
|
profile[key] = validate_version.pop(key)
|
||||||
|
|
||||||
|
validate_version["plugin_state_profiles"] = [profile]
|
||||||
|
|
||||||
|
|
||||||
|
def _conver_publish_plugins(overrides):
|
||||||
|
if "publish" not in overrides:
|
||||||
|
return
|
||||||
|
_convert_validate_version_0_3_3(overrides["publish"])
|
||||||
|
|
||||||
|
|
||||||
|
def convert_settings_overrides(
|
||||||
|
source_version: str,
|
||||||
|
overrides: dict[str, Any],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
_convert_imageio_configs_0_3_1(overrides)
|
||||||
|
_conver_publish_plugins(overrides)
|
||||||
|
return overrides
|
||||||
|
|
@ -54,9 +54,67 @@ class CoreImageIOFileRulesModel(BaseSettingsModel):
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
class CoreImageIOConfigModel(BaseSettingsModel):
|
def _ocio_config_profile_types():
|
||||||
filepath: list[str] = SettingsField(
|
return [
|
||||||
default_factory=list, title="Config path"
|
{"value": "builtin_path", "label": "AYON built-in OCIO config"},
|
||||||
|
{"value": "custom_path", "label": "Path to OCIO config"},
|
||||||
|
{"value": "product_name", "label": "Published product"},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _ocio_built_in_paths():
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"value": "{BUILTIN_OCIO_ROOT}/aces_1.2/config.ocio",
|
||||||
|
"label": "ACES 1.2",
|
||||||
|
"description": "Aces 1.2 OCIO config file."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "{BUILTIN_OCIO_ROOT}/nuke-default/config.ocio",
|
||||||
|
"label": "Nuke default",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class CoreImageIOConfigProfilesModel(BaseSettingsModel):
|
||||||
|
_layout = "expanded"
|
||||||
|
host_names: list[str] = SettingsField(
|
||||||
|
default_factory=list,
|
||||||
|
title="Host names"
|
||||||
|
)
|
||||||
|
task_types: list[str] = SettingsField(
|
||||||
|
default_factory=list,
|
||||||
|
title="Task types",
|
||||||
|
enum_resolver=task_types_enum
|
||||||
|
)
|
||||||
|
task_names: list[str] = SettingsField(
|
||||||
|
default_factory=list,
|
||||||
|
title="Task names"
|
||||||
|
)
|
||||||
|
type: str = SettingsField(
|
||||||
|
title="Profile type",
|
||||||
|
enum_resolver=_ocio_config_profile_types,
|
||||||
|
conditionalEnum=True,
|
||||||
|
default="builtin_path",
|
||||||
|
section="---",
|
||||||
|
)
|
||||||
|
builtin_path: str = SettingsField(
|
||||||
|
"ACES 1.2",
|
||||||
|
title="Built-in OCIO config",
|
||||||
|
enum_resolver=_ocio_built_in_paths,
|
||||||
|
)
|
||||||
|
custom_path: str = SettingsField(
|
||||||
|
"",
|
||||||
|
title="OCIO config path",
|
||||||
|
description="Path to OCIO config. Anatomy formatting is supported.",
|
||||||
|
)
|
||||||
|
product_name: str = SettingsField(
|
||||||
|
"",
|
||||||
|
title="Product name",
|
||||||
|
description=(
|
||||||
|
"Published product name to get OCIO config from. "
|
||||||
|
"Partial match is supported."
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -65,9 +123,8 @@ class CoreImageIOBaseModel(BaseSettingsModel):
|
||||||
False,
|
False,
|
||||||
title="Enable Color Management"
|
title="Enable Color Management"
|
||||||
)
|
)
|
||||||
ocio_config: CoreImageIOConfigModel = SettingsField(
|
ocio_config_profiles: list[CoreImageIOConfigProfilesModel] = SettingsField(
|
||||||
default_factory=CoreImageIOConfigModel,
|
default_factory=list, title="OCIO config profiles"
|
||||||
title="OCIO config"
|
|
||||||
)
|
)
|
||||||
file_rules: CoreImageIOFileRulesModel = SettingsField(
|
file_rules: CoreImageIOFileRulesModel = SettingsField(
|
||||||
default_factory=CoreImageIOFileRulesModel,
|
default_factory=CoreImageIOFileRulesModel,
|
||||||
|
|
@ -186,12 +243,17 @@ class CoreSettings(BaseSettingsModel):
|
||||||
DEFAULT_VALUES = {
|
DEFAULT_VALUES = {
|
||||||
"imageio": {
|
"imageio": {
|
||||||
"activate_global_color_management": False,
|
"activate_global_color_management": False,
|
||||||
"ocio_config": {
|
"ocio_config_profiles": [
|
||||||
"filepath": [
|
{
|
||||||
"{BUILTIN_OCIO_ROOT}/aces_1.2/config.ocio",
|
"host_names": [],
|
||||||
"{BUILTIN_OCIO_ROOT}/nuke-default/config.ocio"
|
"task_types": [],
|
||||||
]
|
"task_names": [],
|
||||||
},
|
"type": "builtin_path",
|
||||||
|
"builtin_path": "{BUILTIN_OCIO_ROOT}/aces_1.2/config.ocio",
|
||||||
|
"custom_path": "",
|
||||||
|
"product_name": "",
|
||||||
|
}
|
||||||
|
],
|
||||||
"file_rules": {
|
"file_rules": {
|
||||||
"activate_global_file_rules": False,
|
"activate_global_file_rules": False,
|
||||||
"rules": [
|
"rules": [
|
||||||
|
|
@ -199,42 +261,57 @@ DEFAULT_VALUES = {
|
||||||
"name": "example",
|
"name": "example",
|
||||||
"pattern": ".*(beauty).*",
|
"pattern": ".*(beauty).*",
|
||||||
"colorspace": "ACES - ACEScg",
|
"colorspace": "ACES - ACEScg",
|
||||||
"ext": "exr"
|
"ext": "exr",
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
"studio_name": "",
|
"studio_name": "",
|
||||||
"studio_code": "",
|
"studio_code": "",
|
||||||
"environments": "{\n\"STUDIO_SW\": {\n \"darwin\": \"/mnt/REPO_SW\",\n \"linux\": \"/mnt/REPO_SW\",\n \"windows\": \"P:/REPO_SW\"\n }\n}",
|
"environments": json.dumps(
|
||||||
|
{
|
||||||
|
"STUDIO_SW": {
|
||||||
|
"darwin": "/mnt/REPO_SW",
|
||||||
|
"linux": "/mnt/REPO_SW",
|
||||||
|
"windows": "P:/REPO_SW"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
indent=4
|
||||||
|
),
|
||||||
"tools": DEFAULT_TOOLS_VALUES,
|
"tools": DEFAULT_TOOLS_VALUES,
|
||||||
"version_start_category": {
|
"version_start_category": {
|
||||||
"profiles": []
|
"profiles": []
|
||||||
},
|
},
|
||||||
"publish": DEFAULT_PUBLISH_VALUES,
|
"publish": DEFAULT_PUBLISH_VALUES,
|
||||||
"project_folder_structure": json.dumps({
|
"project_folder_structure": json.dumps(
|
||||||
"__project_root__": {
|
{
|
||||||
"prod": {},
|
"__project_root__": {
|
||||||
"resources": {
|
"prod": {},
|
||||||
"footage": {
|
"resources": {
|
||||||
"plates": {},
|
"footage": {
|
||||||
"offline": {}
|
"plates": {},
|
||||||
|
"offline": {}
|
||||||
|
},
|
||||||
|
"audio": {},
|
||||||
|
"art_dept": {}
|
||||||
},
|
},
|
||||||
"audio": {},
|
"editorial": {},
|
||||||
"art_dept": {}
|
"assets": {
|
||||||
},
|
"characters": {},
|
||||||
"editorial": {},
|
"locations": {}
|
||||||
"assets": {
|
},
|
||||||
"characters": {},
|
"shots": {}
|
||||||
"locations": {}
|
}
|
||||||
},
|
},
|
||||||
"shots": {}
|
indent=4
|
||||||
}
|
),
|
||||||
}, indent=4),
|
|
||||||
"project_plugins": {
|
"project_plugins": {
|
||||||
"windows": [],
|
"windows": [],
|
||||||
"darwin": [],
|
"darwin": [],
|
||||||
"linux": []
|
"linux": []
|
||||||
},
|
},
|
||||||
"project_environments": "{}"
|
"project_environments": json.dumps(
|
||||||
|
{},
|
||||||
|
indent=4
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,28 @@ class CollectFramesFixDefModel(BaseSettingsModel):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PluginStateByHostModelProfile(BaseSettingsModel):
|
||||||
|
_layout = "expanded"
|
||||||
|
# Filtering
|
||||||
|
host_names: list[str] = SettingsField(
|
||||||
|
default_factory=list,
|
||||||
|
title="Host names"
|
||||||
|
)
|
||||||
|
# Profile values
|
||||||
|
enabled: bool = SettingsField(True, title="Enabled")
|
||||||
|
optional: bool = SettingsField(True, title="Optional")
|
||||||
|
active: bool = SettingsField(True, title="Active")
|
||||||
|
|
||||||
|
|
||||||
|
class PluginStateByHostModel(BaseSettingsModel):
|
||||||
|
_isGroup = True
|
||||||
|
plugin_state_profiles: list[PluginStateByHostModelProfile] = SettingsField(
|
||||||
|
default_factory=list,
|
||||||
|
title="Plugin enable state profiles",
|
||||||
|
description="Change plugin state based on host name."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ValidateIntentProfile(BaseSettingsModel):
|
class ValidateIntentProfile(BaseSettingsModel):
|
||||||
_layout = "expanded"
|
_layout = "expanded"
|
||||||
hosts: list[str] = SettingsField(default_factory=list, title="Host names")
|
hosts: list[str] = SettingsField(default_factory=list, title="Host names")
|
||||||
|
|
@ -536,7 +558,7 @@ class ExtractBurninProfile(BaseSettingsModel):
|
||||||
_layout = "expanded"
|
_layout = "expanded"
|
||||||
product_types: list[str] = SettingsField(
|
product_types: list[str] = SettingsField(
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
title="Produt types"
|
title="Product types"
|
||||||
)
|
)
|
||||||
hosts: list[str] = SettingsField(
|
hosts: list[str] = SettingsField(
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
|
|
@ -766,9 +788,17 @@ class PublishPuginsModel(BaseSettingsModel):
|
||||||
default_factory=ValidateBaseModel,
|
default_factory=ValidateBaseModel,
|
||||||
title="Validate Editorial Asset Name"
|
title="Validate Editorial Asset Name"
|
||||||
)
|
)
|
||||||
ValidateVersion: ValidateBaseModel = SettingsField(
|
ValidateVersion: PluginStateByHostModel = SettingsField(
|
||||||
default_factory=ValidateBaseModel,
|
default_factory=PluginStateByHostModel,
|
||||||
title="Validate Version"
|
title="Validate Version",
|
||||||
|
description=(
|
||||||
|
"Validate that product version to integrate"
|
||||||
|
" is newer than latest version in AYON."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
ValidateOutdatedContainers: PluginStateByHostModel = SettingsField(
|
||||||
|
default_factory=PluginStateByHostModel,
|
||||||
|
title="Validate Containers"
|
||||||
)
|
)
|
||||||
ValidateIntent: ValidateIntentModel = SettingsField(
|
ValidateIntent: ValidateIntentModel = SettingsField(
|
||||||
default_factory=ValidateIntentModel,
|
default_factory=ValidateIntentModel,
|
||||||
|
|
@ -851,9 +881,40 @@ DEFAULT_PUBLISH_VALUES = {
|
||||||
"active": True
|
"active": True
|
||||||
},
|
},
|
||||||
"ValidateVersion": {
|
"ValidateVersion": {
|
||||||
"enabled": True,
|
"plugin_state_profiles": [
|
||||||
"optional": False,
|
{
|
||||||
"active": True
|
"host_names": [
|
||||||
|
"aftereffects",
|
||||||
|
"blender",
|
||||||
|
"houdini",
|
||||||
|
"maya",
|
||||||
|
"nuke",
|
||||||
|
"photoshop",
|
||||||
|
],
|
||||||
|
"enabled": True,
|
||||||
|
"optional": False,
|
||||||
|
"active": True
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ValidateOutdatedContainers": {
|
||||||
|
"plugin_state_profiles": [
|
||||||
|
{
|
||||||
|
# Default host names are based on original
|
||||||
|
# filter of ValidateContainer pyblish plugin
|
||||||
|
"host_names": [
|
||||||
|
"maya",
|
||||||
|
"houdini",
|
||||||
|
"nuke",
|
||||||
|
"harmony",
|
||||||
|
"photoshop",
|
||||||
|
"aftereffects"
|
||||||
|
],
|
||||||
|
"enabled": True,
|
||||||
|
"optional": True,
|
||||||
|
"active": True
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"ValidateIntent": {
|
"ValidateIntent": {
|
||||||
"enabled": False,
|
"enabled": False,
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
name = "aftereffects"
|
name = "aftereffects"
|
||||||
title = "AfterEffects"
|
title = "AfterEffects"
|
||||||
version = "0.1.3"
|
version = "0.1.4"
|
||||||
|
|
|
||||||
|
|
@ -22,12 +22,6 @@ class ValidateSceneSettingsModel(BaseSettingsModel):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ValidateContainersModel(BaseSettingsModel):
|
|
||||||
enabled: bool = SettingsField(True, title="Enabled")
|
|
||||||
optional: bool = SettingsField(True, title="Optional")
|
|
||||||
active: bool = SettingsField(True, title="Active")
|
|
||||||
|
|
||||||
|
|
||||||
class AfterEffectsPublishPlugins(BaseSettingsModel):
|
class AfterEffectsPublishPlugins(BaseSettingsModel):
|
||||||
CollectReview: CollectReviewPluginModel = SettingsField(
|
CollectReview: CollectReviewPluginModel = SettingsField(
|
||||||
default_factory=CollectReviewPluginModel,
|
default_factory=CollectReviewPluginModel,
|
||||||
|
|
@ -37,10 +31,6 @@ class AfterEffectsPublishPlugins(BaseSettingsModel):
|
||||||
default_factory=ValidateSceneSettingsModel,
|
default_factory=ValidateSceneSettingsModel,
|
||||||
title="Validate Scene Settings",
|
title="Validate Scene Settings",
|
||||||
)
|
)
|
||||||
ValidateContainers: ValidateContainersModel = SettingsField(
|
|
||||||
default_factory=ValidateContainersModel,
|
|
||||||
title="Validate Containers",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
AE_PUBLISH_PLUGINS_DEFAULTS = {
|
AE_PUBLISH_PLUGINS_DEFAULTS = {
|
||||||
|
|
@ -58,9 +48,4 @@ AE_PUBLISH_PLUGINS_DEFAULTS = {
|
||||||
".*"
|
".*"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"ValidateContainers": {
|
|
||||||
"enabled": True,
|
|
||||||
"optional": True,
|
|
||||||
"active": True,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -212,7 +212,13 @@ class ApplicationsAddonSettings(BaseSettingsModel):
|
||||||
scope=["studio"]
|
scope=["studio"]
|
||||||
)
|
)
|
||||||
only_available: bool = SettingsField(
|
only_available: bool = SettingsField(
|
||||||
True, title="Show only available applications")
|
True,
|
||||||
|
title="Show only available applications",
|
||||||
|
description="Enable to show only applications in AYON Launcher"
|
||||||
|
" for which the executable paths are found on the running machine."
|
||||||
|
" This applies as an additional filter to the applications defined in a "
|
||||||
|
" project's anatomy settings to ignore unavailable applications."
|
||||||
|
)
|
||||||
|
|
||||||
@validator("tool_groups")
|
@validator("tool_groups")
|
||||||
def validate_unique_name(cls, value):
|
def validate_unique_name(cls, value):
|
||||||
|
|
|
||||||
13
server_addon/blender/client/ayon_blender/__init__.py
Normal file
13
server_addon/blender/client/ayon_blender/__init__.py
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
from .version import __version__
|
||||||
|
from .addon import (
|
||||||
|
BlenderAddon,
|
||||||
|
BLENDER_ADDON_ROOT,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
"__version__",
|
||||||
|
|
||||||
|
"BlenderAddon",
|
||||||
|
"BLENDER_ADDON_ROOT",
|
||||||
|
)
|
||||||
|
|
@ -1,18 +1,21 @@
|
||||||
import os
|
import os
|
||||||
from ayon_core.addon import AYONAddon, IHostAddon
|
from ayon_core.addon import AYONAddon, IHostAddon
|
||||||
|
|
||||||
BLENDER_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
|
from .version import __version__
|
||||||
|
|
||||||
|
BLENDER_ADDON_ROOT = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
|
||||||
class BlenderAddon(AYONAddon, IHostAddon):
|
class BlenderAddon(AYONAddon, IHostAddon):
|
||||||
name = "blender"
|
name = "blender"
|
||||||
|
version = __version__
|
||||||
host_name = "blender"
|
host_name = "blender"
|
||||||
|
|
||||||
def add_implementation_envs(self, env, _app):
|
def add_implementation_envs(self, env, _app):
|
||||||
"""Modify environments to contain all required for implementation."""
|
"""Modify environments to contain all required for implementation."""
|
||||||
# Prepare path to implementation script
|
# Prepare path to implementation script
|
||||||
implementation_user_script_path = os.path.join(
|
implementation_user_script_path = os.path.join(
|
||||||
BLENDER_ROOT_DIR,
|
BLENDER_ADDON_ROOT,
|
||||||
"blender_addon"
|
"blender_addon"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -61,7 +64,7 @@ class BlenderAddon(AYONAddon, IHostAddon):
|
||||||
if app.host_name != self.host_name:
|
if app.host_name != self.host_name:
|
||||||
return []
|
return []
|
||||||
return [
|
return [
|
||||||
os.path.join(BLENDER_ROOT_DIR, "hooks")
|
os.path.join(BLENDER_ADDON_ROOT, "hooks")
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_workfile_extensions(self):
|
def get_workfile_extensions(self):
|
||||||
|
|
@ -15,7 +15,6 @@ from .pipeline import (
|
||||||
|
|
||||||
from .plugin import (
|
from .plugin import (
|
||||||
Creator,
|
Creator,
|
||||||
Loader,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
from .workio import (
|
from .workio import (
|
||||||
|
|
@ -51,7 +50,6 @@ __all__ = [
|
||||||
"BlenderHost",
|
"BlenderHost",
|
||||||
|
|
||||||
"Creator",
|
"Creator",
|
||||||
"Loader",
|
|
||||||
|
|
||||||
# Workfiles API
|
# Workfiles API
|
||||||
"open_file",
|
"open_file",
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue