Merge branch 'develop' into bugfix/AY-5562_Maya-duplicate-thumbnail-integration-error

This commit is contained in:
Roy Nieterau 2024-06-25 15:23:21 +02:00 committed by GitHub
commit 8461bd0b83
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 157 additions and 314 deletions

View file

@ -2,6 +2,8 @@
from .cache import CacheItem, NestedCacheItem
from .projects import (
StatusItem,
StatusStates,
ProjectItem,
ProjectsModel,
PROJECTS_MODEL_SENDER,
@ -21,6 +23,8 @@ __all__ = (
"CacheItem",
"NestedCacheItem",
"StatusItem",
"StatusStates",
"ProjectItem",
"ProjectsModel",
"PROJECTS_MODEL_SENDER",

View file

@ -1,8 +1,8 @@
import contextlib
from abc import ABCMeta, abstractmethod
from abc import ABC, abstractmethod
from typing import Dict, Any
import ayon_api
import six
from ayon_core.style import get_default_entity_icon_color
from ayon_core.lib import CacheItem, NestedCacheItem
@ -10,8 +10,14 @@ from ayon_core.lib import CacheItem, NestedCacheItem
PROJECTS_MODEL_SENDER = "projects.model"
@six.add_metaclass(ABCMeta)
class AbstractHierarchyController:
class StatusStates:
not_started = "not_started"
in_progress = "in_progress"
done = "done"
blocked = "blocked"
class AbstractHierarchyController(ABC):
@abstractmethod
def emit_event(self, topic, data, source):
pass
@ -25,18 +31,24 @@ class StatusItem:
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.
state (str): 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 __init__(
self,
name: str,
color: str,
short: str,
icon: str,
state: str
):
self.name: str = name
self.color: str = color
self.short: str = short
self.icon: str = icon
self.state: str = state
def to_data(self):
def to_data(self) -> Dict[str, Any]:
return {
"name": self.name,
"color": self.color,

View file

@ -217,7 +217,9 @@ class InventoryModel(QtGui.QStandardItemModel):
version_label = format_version(version_item.version)
is_hero = version_item.version < 0
is_latest = version_item.is_latest
if not is_latest:
# TODO maybe use different colors for last approved and last
# version? Or don't care about color at all?
if not is_latest and not version_item.is_last_approved:
version_color = self.OUTDATED_COLOR
status_name = version_item.status

View file

@ -3,7 +3,9 @@ import collections
import ayon_api
from ayon_api.graphql import GraphQlQuery
from ayon_core.host import ILoadHost
from ayon_core.tools.common_models.projects import StatusStates
# --- Implementation that should be in ayon-python-api ---
@ -149,26 +151,35 @@ class RepresentationInfo:
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
def __init__(
self,
version_id: str,
product_id: str,
version: int,
status: str,
is_latest: bool,
is_last_approved: bool,
):
self.version_id: str = version_id
self.product_id: str = product_id
self.version: int = version
self.status: str = status
self.is_latest: bool = is_latest
self.is_last_approved: bool = is_last_approved
@property
def is_hero(self):
return self.version < 0
@classmethod
def from_entity(cls, version_entity, is_latest):
def from_entity(cls, version_entity, is_latest, is_last_approved):
return cls(
version_id=version_entity["id"],
product_id=version_entity["productId"],
version=version_entity["version"],
status=version_entity["status"],
is_latest=is_latest,
is_last_approved=is_last_approved,
)
@ -275,6 +286,11 @@ class ContainersModel:
if product_id not in self._version_items_by_product_id
}
if missing_ids:
status_items_by_name = {
status_item.name: status_item
for status_item in self._controller.get_project_status_items()
}
def version_sorted(entity):
return entity["version"]
@ -300,9 +316,21 @@ class ContainersModel:
version_entities_by_product_id.items()
):
last_version = abs(version_entities[-1]["version"])
last_approved_id = None
for version_entity in version_entities:
status_item = status_items_by_name.get(
version_entity["status"]
)
if status_item is None:
continue
if status_item.state == StatusStates.done:
last_approved_id = version_entity["id"]
version_items_by_id = {
entity["id"]: VersionItem.from_entity(
entity, abs(entity["version"]) == last_version
entity,
abs(entity["version"]) == last_version,
entity["id"] == last_approved_id
)
for entity in version_entities
}

View file

@ -233,19 +233,38 @@ class SceneInventoryView(QtWidgets.QTreeView):
has_outdated = False
has_loaded_hero_versions = False
has_available_hero_version = False
for version_items_by_id in version_items_by_product_id.values():
has_outdated_approved = False
last_version_by_product_id = {}
for product_id, version_items_by_id in (
version_items_by_product_id.items()
):
_has_outdated_approved = False
_last_approved_version_item = None
for version_item in version_items_by_id.values():
if version_item.is_hero:
has_available_hero_version = True
elif version_item.is_last_approved:
_last_approved_version_item = version_item
_has_outdated_approved = True
if version_item.version_id not in version_ids:
continue
if version_item.is_hero:
has_loaded_hero_versions = True
elif not version_item.is_latest:
has_outdated = True
if (
_has_outdated_approved
and _last_approved_version_item is not None
):
last_version_by_product_id[product_id] = (
_last_approved_version_item
)
has_outdated_approved = True
switch_to_versioned = None
if has_loaded_hero_versions:
update_icon = qtawesome.icon(
@ -261,6 +280,42 @@ class SceneInventoryView(QtWidgets.QTreeView):
lambda: self._on_switch_to_versioned(item_ids)
)
update_to_last_approved_action = None
approved_version_by_item_id = {}
if has_outdated_approved:
for container_item in container_items_by_id.values():
repre_id = container_item.representation_id
repre_info = repre_info_by_id.get(repre_id)
if not repre_info or not repre_info.is_valid:
continue
version_item = last_version_by_product_id.get(
repre_info.product_id
)
if (
version_item is None
or version_item.version_id == repre_info.version_id
):
continue
approved_version_by_item_id[container_item.item_id] = (
version_item.version
)
if approved_version_by_item_id:
update_icon = qtawesome.icon(
"fa.angle-double-up",
color="#00f0b4"
)
update_to_last_approved_action = QtWidgets.QAction(
update_icon,
"Update to last approved",
menu
)
update_to_last_approved_action.triggered.connect(
lambda: self._update_containers_to_approved_versions(
approved_version_by_item_id
)
)
update_to_latest_action = None
if has_outdated or has_loaded_hero_versions:
update_icon = qtawesome.icon(
@ -299,7 +354,9 @@ class SceneInventoryView(QtWidgets.QTreeView):
# set version
set_version_action = None
if active_repre_id is not None:
set_version_icon = qtawesome.icon("fa.hashtag", color=DEFAULT_COLOR)
set_version_icon = qtawesome.icon(
"fa.hashtag", color=DEFAULT_COLOR
)
set_version_action = QtWidgets.QAction(
set_version_icon,
"Set version",
@ -323,6 +380,9 @@ class SceneInventoryView(QtWidgets.QTreeView):
if switch_to_versioned:
menu.addAction(switch_to_versioned)
if update_to_last_approved_action:
menu.addAction(update_to_last_approved_action)
if update_to_latest_action:
menu.addAction(update_to_latest_action)
@ -970,3 +1030,24 @@ class SceneInventoryView(QtWidgets.QTreeView):
"""
versions = [version for _ in range(len(item_ids))]
self._update_containers(item_ids, versions)
def _update_containers_to_approved_versions(
self, approved_version_by_item_id
):
"""Helper to update items to given version (or version per item)
If at least one item is specified this will always try to refresh
the inventory even if errors occurred on any of the items.
Arguments:
approved_version_by_item_id (Dict[str, int]): Version to set by
item id.
"""
versions = []
item_ids = []
for item_id, version in approved_version_by_item_id.items():
item_ids.append(item_id)
versions.append(version)
self._update_containers(item_ids, versions)

View file

@ -4,10 +4,6 @@ from .pipeline import (
containerise
)
from .plugin import (
Creator,
)
from .lib import (
lsattr,
lsattrs,
@ -23,8 +19,6 @@ __all__ = [
"ls",
"containerise",
"Creator",
# Utility functions
"lsattr",
"lsattrs",

View file

@ -148,89 +148,6 @@ def validate_fps():
return True
def create_remote_publish_node(force=True):
"""Function to create a remote publish node in /out
This is a hacked "Shell" node that does *nothing* except for triggering
`colorbleed.lib.publish_remote()` as pre-render script.
All default attributes of the Shell node are hidden to the Artist to
avoid confusion.
Additionally some custom attributes are added that can be collected
by a Collector to set specific settings for the publish, e.g. whether
to separate the jobs per instance or process in one single job.
"""
cmd = "import colorbleed.lib; colorbleed.lib.publish_remote()"
existing = hou.node("/out/REMOTE_PUBLISH")
if existing:
if force:
log.warning("Removing existing '/out/REMOTE_PUBLISH' node..")
existing.destroy()
else:
raise RuntimeError("Node already exists /out/REMOTE_PUBLISH. "
"Please remove manually or set `force` to "
"True.")
# Create the shell node
out = hou.node("/out")
node = out.createNode("shell", node_name="REMOTE_PUBLISH")
node.moveToGoodPosition()
# Set color make it stand out (avalon/pyblish color)
node.setColor(hou.Color(0.439, 0.709, 0.933))
# Set the pre-render script
node.setParms({
"prerender": cmd,
"lprerender": "python" # command language
})
# Lock the attributes to ensure artists won't easily mess things up.
node.parm("prerender").lock(True)
node.parm("lprerender").lock(True)
# Lock up the actual shell command
command_parm = node.parm("command")
command_parm.set("")
command_parm.lock(True)
shellexec_parm = node.parm("shellexec")
shellexec_parm.set(False)
shellexec_parm.lock(True)
# Get the node's parm template group so we can customize it
template = node.parmTemplateGroup()
# Hide default tabs
template.hideFolder("Shell", True)
template.hideFolder("Scripts", True)
# Hide default settings
template.hide("execute", True)
template.hide("renderdialog", True)
template.hide("trange", True)
template.hide("f", True)
template.hide("take", True)
# Add custom settings to this node.
parm_folder = hou.FolderParmTemplate("folder", "Submission Settings")
# Separate Jobs per Instance
parm = hou.ToggleParmTemplate(name="separateJobPerInstance",
label="Separate Job per Instance",
default_value=False)
parm_folder.addParmTemplate(parm)
# Add our custom Submission Settings folder
template.append(parm_folder)
# Apply template back to the node
node.setParmTemplateGroup(template)
def render_rop(ropnode):
"""Render ROP node utility for Publishing.

View file

@ -10,8 +10,7 @@ import hou
import pyblish.api
from ayon_core.pipeline import (
CreatorError,
LegacyCreator,
Creator as NewCreator,
Creator,
CreatedInstance,
AYON_INSTANCE_ID,
AVALON_INSTANCE_ID,
@ -26,80 +25,6 @@ from .lib import imprint, read, lsattr, add_self_publish_button
SETTINGS_CATEGORY = "houdini"
class Creator(LegacyCreator):
"""Creator plugin to create instances in Houdini
To support the wide range of node types for render output (Alembic, VDB,
Mantra) the Creator needs a node type to create the correct instance
By default, if none is given, is `geometry`. An example of accepted node
types: geometry, alembic, ifd (mantra)
Please check the Houdini documentation for more node types.
Tip: to find the exact node type to create press the `i` left of the node
when hovering over a node. The information is visible under the name of
the node.
Deprecated:
This creator is deprecated and will be removed in future version.
"""
defaults = ['Main']
def __init__(self, *args, **kwargs):
super(Creator, self).__init__(*args, **kwargs)
self.nodes = []
def process(self):
"""This is the base functionality to create instances in Houdini
The selected nodes are stored in self to be used in an override method.
This is currently necessary in order to support the multiple output
types in Houdini which can only be rendered through their own node.
Default node type if none is given is `geometry`
It also makes it easier to apply custom settings per instance type
Example of override method for Alembic:
def process(self):
instance = super(CreateEpicNode, self, process()
# Set parameters for Alembic node
instance.setParms(
{"sop_path": "$HIP/%s.abc" % self.nodes[0]}
)
Returns:
hou.Node
"""
try:
if (self.options or {}).get("useSelection"):
self.nodes = hou.selectedNodes()
# Get the node type and remove it from the data, not needed
node_type = self.data.pop("node_type", None)
if node_type is None:
node_type = "geometry"
# Get out node
out = hou.node("/out")
instance = out.createNode(node_type, node_name=self.name)
instance.moveToGoodPosition()
imprint(instance, self.data)
self._process(instance)
except hou.Error as er:
six.reraise(
CreatorError,
CreatorError("Creator error: {}".format(er)),
sys.exc_info()[2])
class HoudiniCreatorBase(object):
@staticmethod
def cache_instance_data(shared_data):
@ -175,7 +100,7 @@ class HoudiniCreatorBase(object):
@six.add_metaclass(ABCMeta)
class HoudiniCreator(NewCreator, HoudiniCreatorBase):
class HoudiniCreator(Creator, HoudiniCreatorBase):
"""Base class for most of the Houdini creator plugins."""
selected_nodes = []
settings_name = None

View file

@ -1,29 +0,0 @@
import hou
import pyblish.api
from ayon_core.pipeline.publish import RepairAction
from ayon_houdini.api import lib, plugin
class CollectRemotePublishSettings(plugin.HoudiniContextPlugin):
"""Collect custom settings of the Remote Publish node."""
order = pyblish.api.CollectorOrder
families = ["*"]
targets = ["deadline"]
label = "Remote Publish Submission Settings"
actions = [RepairAction]
def process(self, context):
node = hou.node("/out/REMOTE_PUBLISH")
if not node:
return
attributes = lib.read(node)
# Debug the settings we have collected
for key, value in sorted(attributes.items()):
self.log.debug("Collected %s: %s" % (key, value))
context.data.update(attributes)

View file

@ -1,50 +0,0 @@
# -*-coding: utf-8 -*-
import hou
import pyblish.api
from ayon_core.pipeline.publish import RepairContextAction
from ayon_core.pipeline import PublishValidationError
from ayon_houdini.api import lib, plugin
class ValidateRemotePublishOutNode(plugin.HoudiniContextPlugin):
"""Validate the remote publish out node exists for Deadline to trigger."""
order = pyblish.api.ValidatorOrder - 0.4
families = ["*"]
targets = ["deadline"]
label = "Remote Publish ROP node"
actions = [RepairContextAction]
def process(self, context):
cmd = "import colorbleed.lib; colorbleed.lib.publish_remote()"
node = hou.node("/out/REMOTE_PUBLISH")
if not node:
raise RuntimeError("Missing REMOTE_PUBLISH node.")
# We ensure it's a shell node and that it has the pre-render script
# set correctly. Plus the shell script it will trigger should be
# completely empty (doing nothing)
if node.type().name() != "shell":
self.raise_error("Must be shell ROP node")
if node.parm("command").eval() != "":
self.raise_error("Must have no command")
if node.parm("shellexec").eval():
self.raise_error("Must not execute in shell")
if node.parm("prerender").eval() != cmd:
self.raise_error("REMOTE_PUBLISH node does not have "
"correct prerender script.")
if node.parm("lprerender").eval() != "python":
self.raise_error("REMOTE_PUBLISH node prerender script "
"type not set to 'python'")
@classmethod
def repair(cls, context):
"""(Re)create the node if it fails to pass validation."""
lib.create_remote_publish_node(force=True)
def raise_error(self, message):
raise PublishValidationError(message)

View file

@ -1,41 +0,0 @@
# -*- coding: utf-8 -*-
import hou
import pyblish.api
from ayon_core.pipeline.publish import RepairContextAction
from ayon_core.pipeline import PublishValidationError
from ayon_houdini.api import plugin
class ValidateRemotePublishEnabled(plugin.HoudiniContextPlugin):
"""Validate the remote publish node is *not* bypassed."""
order = pyblish.api.ValidatorOrder - 0.39
families = ["*"]
targets = ["deadline"]
label = "Remote Publish ROP enabled"
actions = [RepairContextAction]
def process(self, context):
node = hou.node("/out/REMOTE_PUBLISH")
if not node:
raise PublishValidationError(
"Missing REMOTE_PUBLISH node.", title=self.label)
if node.isBypassed():
raise PublishValidationError(
"REMOTE_PUBLISH must not be bypassed.", title=self.label)
@classmethod
def repair(cls, context):
"""(Re)create the node if it fails to pass validation."""
node = hou.node("/out/REMOTE_PUBLISH")
if not node:
raise PublishValidationError(
"Missing REMOTE_PUBLISH node.", title=cls.label)
cls.log.info("Disabling bypass on /out/REMOTE_PUBLISH")
node.bypass(False)

View file

@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring AYON addon 'houdini' version."""
__version__ = "0.3.4"
__version__ = "0.3.6"

View file

@ -1,6 +1,6 @@
name = "houdini"
title = "Houdini"
version = "0.3.4"
version = "0.3.6"
client_dir = "ayon_houdini"