mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge branch 'develop' of https://github.com/ynput/ayon-core into enhancement/houdini_load_asset_lop
# Conflicts: # server_addon/houdini/client/ayon_houdini/api/hda_utils.py # server_addon/houdini/client/ayon_houdini/plugins/load/load_asset_lop.py
This commit is contained in:
commit
c2d1470def
1176 changed files with 10029 additions and 5638 deletions
|
|
@ -51,8 +51,22 @@ IGNORED_MODULES_IN_AYON = set()
|
|||
# - this is used to log the missing addon
|
||||
MOVED_ADDON_MILESTONE_VERSIONS = {
|
||||
"applications": VersionInfo(0, 2, 0),
|
||||
"celaction": VersionInfo(0, 2, 0),
|
||||
"clockify": VersionInfo(0, 2, 0),
|
||||
"flame": VersionInfo(0, 2, 0),
|
||||
"fusion": 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
|
||||
class _ModuleClass(object):
|
||||
"""Fake module class for storing AYON addons.
|
||||
|
|
@ -1321,7 +1335,7 @@ class TrayAddonsManager(AddonsManager):
|
|||
self.doubleclick_callback = None
|
||||
|
||||
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
|
||||
callback can be defined with `doubleclick_callback` attribute.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from ayon_applications import PreLaunchHook
|
||||
|
||||
from ayon_core.pipeline.colorspace import get_imageio_config
|
||||
from ayon_core.pipeline.template_data import get_template_data_with_names
|
||||
from ayon_core.pipeline.colorspace import get_imageio_config_preset
|
||||
from ayon_core.pipeline.template_data import get_template_data
|
||||
|
||||
|
||||
class OCIOEnvHook(PreLaunchHook):
|
||||
|
|
@ -26,32 +26,38 @@ class OCIOEnvHook(PreLaunchHook):
|
|||
def execute(self):
|
||||
"""Hook entry method."""
|
||||
|
||||
template_data = get_template_data_with_names(
|
||||
project_name=self.data["project_name"],
|
||||
folder_path=self.data["folder_path"],
|
||||
task_name=self.data["task_name"],
|
||||
folder_entity = self.data["folder_entity"]
|
||||
|
||||
template_data = get_template_data(
|
||||
self.data["project_entity"],
|
||||
folder_entity=folder_entity,
|
||||
task_entity=self.data["task_entity"],
|
||||
host_name=self.host_name,
|
||||
settings=self.data["project_settings"]
|
||||
settings=self.data["project_settings"],
|
||||
)
|
||||
|
||||
config_data = get_imageio_config(
|
||||
project_name=self.data["project_name"],
|
||||
host_name=self.host_name,
|
||||
project_settings=self.data["project_settings"],
|
||||
anatomy_data=template_data,
|
||||
config_data = get_imageio_config_preset(
|
||||
self.data["project_name"],
|
||||
self.data["folder_path"],
|
||||
self.data["task_name"],
|
||||
self.host_name,
|
||||
anatomy=self.data["anatomy"],
|
||||
project_settings=self.data["project_settings"],
|
||||
template_data=template_data,
|
||||
env=self.launch_context.env,
|
||||
folder_id=folder_entity["id"],
|
||||
)
|
||||
|
||||
if 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:
|
||||
if not config_data:
|
||||
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
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ def main(*subprocess_args):
|
|||
)
|
||||
)
|
||||
|
||||
elif os.environ.get("AVALON_PHOTOSHOP_WORKFILES_ON_LAUNCH", True):
|
||||
elif os.environ.get("AVALON_AFTEREFFECTS_WORKFILES_ON_LAUNCH", True):
|
||||
save = False
|
||||
if os.getenv("WORKFILES_SAVE_AS"):
|
||||
save = True
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ def load_scripts(paths):
|
|||
if register:
|
||||
try:
|
||||
register()
|
||||
except:
|
||||
except: # noqa E722
|
||||
traceback.print_exc()
|
||||
else:
|
||||
print("\nWarning! '%s' has no register function, "
|
||||
|
|
@ -45,7 +45,7 @@ def load_scripts(paths):
|
|||
if unregister:
|
||||
try:
|
||||
unregister()
|
||||
except:
|
||||
except: # noqa E722
|
||||
traceback.print_exc()
|
||||
|
||||
def test_reload(mod):
|
||||
|
|
@ -57,7 +57,7 @@ def load_scripts(paths):
|
|||
|
||||
try:
|
||||
return importlib.reload(mod)
|
||||
except:
|
||||
except: # noqa E722
|
||||
traceback.print_exc()
|
||||
|
||||
def test_register(mod):
|
||||
|
|
@ -365,3 +365,62 @@ def maintained_time():
|
|||
yield
|
||||
finally:
|
||||
bpy.context.scene.frame_current = current_time
|
||||
|
||||
|
||||
def get_all_parents(obj):
|
||||
"""Get all recursive parents of object.
|
||||
|
||||
Arguments:
|
||||
obj (bpy.types.Object): Object to get all parents for.
|
||||
|
||||
Returns:
|
||||
List[bpy.types.Object]: All parents of object
|
||||
|
||||
"""
|
||||
result = []
|
||||
while True:
|
||||
obj = obj.parent
|
||||
if not obj:
|
||||
break
|
||||
result.append(obj)
|
||||
return result
|
||||
|
||||
|
||||
def get_highest_root(objects):
|
||||
"""Get the highest object (the least parents) among the objects.
|
||||
|
||||
If multiple objects have the same amount of parents (or no parents) the
|
||||
first object found in the input iterable will be returned.
|
||||
|
||||
Note that this will *not* return objects outside of the input list, as
|
||||
such it will not return the root of node from a child node. It is purely
|
||||
intended to find the highest object among a list of objects. To instead
|
||||
get the root from one object use, e.g. `get_all_parents(obj)[-1]`
|
||||
|
||||
Arguments:
|
||||
objects (List[bpy.types.Object]): Objects to find the highest root in.
|
||||
|
||||
Returns:
|
||||
Optional[bpy.types.Object]: First highest root found or None if no
|
||||
`bpy.types.Object` found in input list.
|
||||
|
||||
"""
|
||||
included_objects = {obj.name_full for obj in objects}
|
||||
num_parents_to_obj = {}
|
||||
for obj in objects:
|
||||
if isinstance(obj, bpy.types.Object):
|
||||
parents = get_all_parents(obj)
|
||||
# included parents
|
||||
parents = [parent for parent in parents if
|
||||
parent.name_full in included_objects]
|
||||
if not parents:
|
||||
# A node without parents must be a highest root
|
||||
return obj
|
||||
|
||||
num_parents_to_obj.setdefault(len(parents), obj)
|
||||
|
||||
if not num_parents_to_obj:
|
||||
return
|
||||
|
||||
minimum_parent = min(num_parents_to_obj)
|
||||
return num_parents_to_obj[minimum_parent]
|
||||
|
|
|
|||
|
|
@ -26,7 +26,8 @@ from .ops import (
|
|||
)
|
||||
from .lib import imprint
|
||||
|
||||
VALID_EXTENSIONS = [".blend", ".json", ".abc", ".fbx"]
|
||||
VALID_EXTENSIONS = [".blend", ".json", ".abc", ".fbx",
|
||||
".usd", ".usdc", ".usda"]
|
||||
|
||||
|
||||
def prepare_scene_name(
|
||||
|
|
@ -143,13 +144,19 @@ def deselect_all():
|
|||
if obj.mode != 'OBJECT':
|
||||
modes.append((obj, obj.mode))
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
context_override = create_blender_context(active=obj)
|
||||
with bpy.context.temp_override(**context_override):
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
context_override = create_blender_context()
|
||||
with bpy.context.temp_override(**context_override):
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
|
||||
for p in modes:
|
||||
bpy.context.view_layer.objects.active = p[0]
|
||||
bpy.ops.object.mode_set(mode=p[1])
|
||||
context_override = create_blender_context(active=p[0])
|
||||
with bpy.context.temp_override(**context_override):
|
||||
bpy.ops.object.mode_set(mode=p[1])
|
||||
|
||||
bpy.context.view_layer.objects.active = active
|
||||
|
||||
|
|
|
|||
30
client/ayon_core/hosts/blender/plugins/create/create_usd.py
Normal file
30
client/ayon_core/hosts/blender/plugins/create/create_usd.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
"""Create a USD Export."""
|
||||
|
||||
from ayon_core.hosts.blender.api import plugin, lib
|
||||
|
||||
|
||||
class CreateUSD(plugin.BaseCreator):
|
||||
"""Create USD Export"""
|
||||
|
||||
identifier = "io.openpype.creators.blender.usd"
|
||||
name = "usdMain"
|
||||
label = "USD"
|
||||
product_type = "usd"
|
||||
icon = "gears"
|
||||
|
||||
def create(
|
||||
self, product_name: str, instance_data: dict, pre_create_data: dict
|
||||
):
|
||||
# Run parent create method
|
||||
collection = super().create(
|
||||
product_name, instance_data, pre_create_data
|
||||
)
|
||||
|
||||
if pre_create_data.get("use_selection"):
|
||||
objects = lib.get_selection()
|
||||
for obj in objects:
|
||||
collection.objects.link(obj)
|
||||
if obj.type == 'EMPTY':
|
||||
objects.extend(obj.children)
|
||||
|
||||
return collection
|
||||
|
|
@ -26,10 +26,10 @@ class CacheModelLoader(plugin.AssetLoader):
|
|||
Note:
|
||||
At least for now it only supports Alembic files.
|
||||
"""
|
||||
product_types = {"model", "pointcache", "animation"}
|
||||
representations = {"abc"}
|
||||
product_types = {"model", "pointcache", "animation", "usd"}
|
||||
representations = {"abc", "usd"}
|
||||
|
||||
label = "Load Alembic"
|
||||
label = "Load Cache"
|
||||
icon = "code-fork"
|
||||
color = "orange"
|
||||
|
||||
|
|
@ -53,10 +53,21 @@ class CacheModelLoader(plugin.AssetLoader):
|
|||
plugin.deselect_all()
|
||||
|
||||
relative = bpy.context.preferences.filepaths.use_relative_paths
|
||||
bpy.ops.wm.alembic_import(
|
||||
filepath=libpath,
|
||||
relative_path=relative
|
||||
)
|
||||
|
||||
if any(libpath.lower().endswith(ext)
|
||||
for ext in [".usd", ".usda", ".usdc"]):
|
||||
# USD
|
||||
bpy.ops.wm.usd_import(
|
||||
filepath=libpath,
|
||||
relative_path=relative
|
||||
)
|
||||
|
||||
else:
|
||||
# Alembic
|
||||
bpy.ops.wm.alembic_import(
|
||||
filepath=libpath,
|
||||
relative_path=relative
|
||||
)
|
||||
|
||||
imported = lib.get_selection()
|
||||
|
||||
|
|
@ -43,7 +43,10 @@ class AbcCameraLoader(plugin.AssetLoader):
|
|||
def _process(self, libpath, asset_group, group_name):
|
||||
plugin.deselect_all()
|
||||
|
||||
bpy.ops.wm.alembic_import(filepath=libpath)
|
||||
# Force the creation of the transform cache even if the camera
|
||||
# doesn't have an animation. We use the cache to update the camera.
|
||||
bpy.ops.wm.alembic_import(
|
||||
filepath=libpath, always_add_cache_reader=True)
|
||||
|
||||
objects = lib.get_selection()
|
||||
|
||||
|
|
@ -178,12 +181,33 @@ class AbcCameraLoader(plugin.AssetLoader):
|
|||
self.log.info("Library already loaded, not updating...")
|
||||
return
|
||||
|
||||
mat = asset_group.matrix_basis.copy()
|
||||
for obj in asset_group.children:
|
||||
found = False
|
||||
for constraint in obj.constraints:
|
||||
if constraint.type == "TRANSFORM_CACHE":
|
||||
constraint.cache_file.filepath = libpath.as_posix()
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
# This is to keep compatibility with cameras loaded with
|
||||
# the old loader
|
||||
# Create a new constraint for the cache file
|
||||
constraint = obj.constraints.new("TRANSFORM_CACHE")
|
||||
bpy.ops.cachefile.open(filepath=libpath.as_posix())
|
||||
constraint.cache_file = bpy.data.cache_files[-1]
|
||||
constraint.cache_file.scale = 1.0
|
||||
|
||||
self._remove(asset_group)
|
||||
self._process(str(libpath), asset_group, object_name)
|
||||
# This is a workaround to set the object path. Blender doesn't
|
||||
# load the list of object paths until the object is evaluated.
|
||||
# This is a hack to force the object to be evaluated.
|
||||
# The modifier doesn't need to be removed because camera
|
||||
# objects don't have modifiers.
|
||||
obj.modifiers.new(
|
||||
name='MeshSequenceCache', type='MESH_SEQUENCE_CACHE')
|
||||
bpy.context.evaluated_depsgraph_get()
|
||||
|
||||
asset_group.matrix_basis = mat
|
||||
constraint.object_path = (
|
||||
constraint.cache_file.object_paths[0].path)
|
||||
|
||||
metadata["libpath"] = str(libpath)
|
||||
metadata["representation"] = repre_entity["id"]
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ class CollectBlenderInstanceData(pyblish.api.InstancePlugin):
|
|||
order = pyblish.api.CollectorOrder
|
||||
hosts = ["blender"]
|
||||
families = ["model", "pointcache", "animation", "rig", "camera", "layout",
|
||||
"blendScene"]
|
||||
"blendScene", "usd"]
|
||||
label = "Collect Instance"
|
||||
|
||||
def process(self, instance):
|
||||
|
|
|
|||
|
|
@ -0,0 +1,90 @@
|
|||
import os
|
||||
|
||||
import bpy
|
||||
|
||||
from ayon_core.pipeline import publish
|
||||
from ayon_core.hosts.blender.api import plugin, lib
|
||||
|
||||
|
||||
class ExtractUSD(publish.Extractor):
|
||||
"""Extract as USD."""
|
||||
|
||||
label = "Extract USD"
|
||||
hosts = ["blender"]
|
||||
families = ["usd"]
|
||||
|
||||
def process(self, instance):
|
||||
|
||||
# Ignore runtime instances (e.g. USD layers)
|
||||
# TODO: This is better done via more specific `families`
|
||||
if not instance.data.get("transientData", {}).get("instance_node"):
|
||||
return
|
||||
|
||||
# Define extract output file path
|
||||
stagingdir = self.staging_dir(instance)
|
||||
filename = f"{instance.name}.usd"
|
||||
filepath = os.path.join(stagingdir, filename)
|
||||
|
||||
# Perform extraction
|
||||
self.log.debug("Performing extraction..")
|
||||
|
||||
# Select all members to "export selected"
|
||||
plugin.deselect_all()
|
||||
|
||||
selected = []
|
||||
for obj in instance:
|
||||
if isinstance(obj, bpy.types.Object):
|
||||
obj.select_set(True)
|
||||
selected.append(obj)
|
||||
|
||||
root = lib.get_highest_root(objects=instance[:])
|
||||
if not root:
|
||||
instance_node = instance.data["transientData"]["instance_node"]
|
||||
raise publish.KnownPublishError(
|
||||
f"No root object found in instance: {instance_node.name}"
|
||||
)
|
||||
self.log.debug(f"Exporting using active root: {root.name}")
|
||||
|
||||
context = plugin.create_blender_context(
|
||||
active=root, selected=selected)
|
||||
|
||||
# Export USD
|
||||
with bpy.context.temp_override(**context):
|
||||
bpy.ops.wm.usd_export(
|
||||
filepath=filepath,
|
||||
selected_objects_only=True,
|
||||
export_textures=False,
|
||||
relative_paths=False,
|
||||
export_animation=False,
|
||||
export_hair=False,
|
||||
export_uvmaps=True,
|
||||
# TODO: add for new version of Blender (4+?)
|
||||
# export_mesh_colors=True,
|
||||
export_normals=True,
|
||||
export_materials=True,
|
||||
use_instancing=True
|
||||
)
|
||||
|
||||
plugin.deselect_all()
|
||||
|
||||
# Add representation
|
||||
representation = {
|
||||
'name': 'usd',
|
||||
'ext': 'usd',
|
||||
'files': filename,
|
||||
"stagingDir": stagingdir,
|
||||
}
|
||||
instance.data.setdefault("representations", []).append(representation)
|
||||
self.log.debug("Extracted instance '%s' to: %s",
|
||||
instance.name, representation)
|
||||
|
||||
|
||||
class ExtractModelUSD(ExtractUSD):
|
||||
"""Extract model as USD."""
|
||||
|
||||
label = "Extract USD (Model)"
|
||||
hosts = ["blender"]
|
||||
families = ["model"]
|
||||
|
||||
# Driven by settings
|
||||
optional = True
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
from .addon import (
|
||||
HOST_DIR,
|
||||
FlameAddon,
|
||||
)
|
||||
|
||||
|
||||
__all__ = (
|
||||
"HOST_DIR",
|
||||
"FlameAddon",
|
||||
)
|
||||
|
|
@ -1110,10 +1110,7 @@ def apply_colorspace_project():
|
|||
'''
|
||||
# backward compatibility layer
|
||||
# TODO: remove this after some time
|
||||
config_data = get_imageio_config(
|
||||
project_name=get_current_project_name(),
|
||||
host_name="hiero"
|
||||
)
|
||||
config_data = get_current_context_imageio_config_preset()
|
||||
|
||||
if config_data:
|
||||
presets.update({
|
||||
|
|
|
|||
|
|
@ -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,98 +0,0 @@
|
|||
import pyblish.api
|
||||
|
||||
from ayon_core.pipeline import OptionalPyblishPluginMixin
|
||||
from ayon_core.pipeline.publish import RepairAction, PublishValidationError
|
||||
|
||||
|
||||
class ValidateAlembicDefaultsPointcache(
|
||||
pyblish.api.InstancePlugin, OptionalPyblishPluginMixin
|
||||
):
|
||||
"""Validate the attributes on the instance are defaults.
|
||||
|
||||
The defaults are defined in the project settings.
|
||||
"""
|
||||
|
||||
order = pyblish.api.ValidatorOrder
|
||||
families = ["pointcache"]
|
||||
hosts = ["maya"]
|
||||
label = "Validate Alembic Options Defaults"
|
||||
actions = [RepairAction]
|
||||
optional = True
|
||||
|
||||
plugin_name = "ExtractAlembic"
|
||||
|
||||
@classmethod
|
||||
def _get_settings(cls, context):
|
||||
maya_settings = context.data["project_settings"]["maya"]
|
||||
settings = maya_settings["publish"]["ExtractAlembic"]
|
||||
return settings
|
||||
|
||||
@classmethod
|
||||
def _get_publish_attributes(cls, instance):
|
||||
attributes = instance.data["publish_attributes"][
|
||||
cls.plugin_name(
|
||||
instance.data["publish_attributes"]
|
||||
)
|
||||
]
|
||||
|
||||
return attributes
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
settings = self._get_settings(instance.context)
|
||||
|
||||
attributes = self._get_publish_attributes(instance)
|
||||
|
||||
msg = (
|
||||
"Alembic Extract setting \"{}\" is not the default value:"
|
||||
"\nCurrent: {}"
|
||||
"\nDefault Value: {}\n"
|
||||
)
|
||||
errors = []
|
||||
for key, value in attributes.items():
|
||||
default_value = settings[key]
|
||||
|
||||
# Lists are best to compared sorted since we cant rely on the order
|
||||
# of the items.
|
||||
if isinstance(value, list):
|
||||
value = sorted(value)
|
||||
default_value = sorted(default_value)
|
||||
|
||||
if value != default_value:
|
||||
errors.append(msg.format(key, value, default_value))
|
||||
|
||||
if errors:
|
||||
raise PublishValidationError("\n".join(errors))
|
||||
|
||||
@classmethod
|
||||
def repair(cls, instance):
|
||||
# Find create instance twin.
|
||||
create_context = instance.context.data["create_context"]
|
||||
create_instance = create_context.get_instance_by_id(
|
||||
instance.data["instance_id"]
|
||||
)
|
||||
|
||||
# Set the settings values on the create context then save to workfile.
|
||||
publish_attributes = instance.data["publish_attributes"]
|
||||
plugin_name = cls.plugin_name(publish_attributes)
|
||||
attributes = cls._get_publish_attributes(instance)
|
||||
settings = cls._get_settings(instance.context)
|
||||
create_publish_attributes = create_instance.data["publish_attributes"]
|
||||
for key in attributes:
|
||||
create_publish_attributes[plugin_name][key] = settings[key]
|
||||
|
||||
create_context.save_changes()
|
||||
|
||||
|
||||
class ValidateAlembicDefaultsAnimation(
|
||||
ValidateAlembicDefaultsPointcache
|
||||
):
|
||||
"""Validate the attributes on the instance are defaults.
|
||||
|
||||
The defaults are defined in the project settings.
|
||||
"""
|
||||
label = "Validate Alembic Options Defaults"
|
||||
families = ["animation"]
|
||||
plugin_name = "ExtractAnimation"
|
||||
|
|
@ -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:
|
||||
"""Get UE Editor executable path."""
|
||||
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 engine_version.split(".")[0] == "4":
|
||||
ue_path /= "Win64/UE4Editor.exe"
|
||||
elif engine_version.split(".")[0] == "5":
|
||||
ue_path /= "Win64/UnrealEditor.exe"
|
||||
ue_path /= f"Win64/{ue_name}.exe"
|
||||
|
||||
elif platform.system().lower() == "linux":
|
||||
ue_path /= "Linux/UE4Editor"
|
||||
ue_path /= f"Linux/{ue_name}"
|
||||
|
||||
elif platform.system().lower() == "darwin":
|
||||
ue_path /= "Mac/UE4Editor"
|
||||
ue_path /= f"Mac/{ue_name}"
|
||||
|
||||
return ue_path
|
||||
|
||||
|
|
|
|||
|
|
@ -139,6 +139,7 @@ from .path_tools import (
|
|||
)
|
||||
|
||||
from .ayon_info import (
|
||||
is_in_ayon_launcher_process,
|
||||
is_running_from_build,
|
||||
is_using_ayon_console,
|
||||
is_staging_enabled,
|
||||
|
|
@ -248,6 +249,7 @@ __all__ = [
|
|||
|
||||
"Logger",
|
||||
|
||||
"is_in_ayon_launcher_process",
|
||||
"is_running_from_build",
|
||||
"is_using_ayon_console",
|
||||
"is_staging_enabled",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import os
|
||||
import sys
|
||||
import json
|
||||
import datetime
|
||||
import platform
|
||||
|
|
@ -25,6 +26,18 @@ def get_ayon_launcher_version():
|
|||
return content["__version__"]
|
||||
|
||||
|
||||
def is_in_ayon_launcher_process():
|
||||
"""Determine if current process is running from AYON launcher.
|
||||
|
||||
Returns:
|
||||
bool: True if running from AYON launcher.
|
||||
|
||||
"""
|
||||
ayon_executable_path = os.path.normpath(os.environ["AYON_EXECUTABLE"])
|
||||
executable_path = os.path.normpath(sys.executable)
|
||||
return ayon_executable_path == executable_path
|
||||
|
||||
|
||||
def is_running_from_build():
|
||||
"""Determine if current process is running from build or code.
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
# 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):
|
||||
"""Wrap request post method.
|
||||
|
||||
Disabling SSL certificate validation if ``DONT_VERIFY_SSL`` environment
|
||||
variable is found. This is useful when Deadline server is
|
||||
Disabling SSL certificate validation if ``verify`` kwarg is set to False.
|
||||
This is useful when Deadline server is
|
||||
running with self-signed certificates and its certificate is not
|
||||
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.
|
||||
|
||||
"""
|
||||
if 'verify' not in kwargs:
|
||||
kwargs['verify'] = False if os.getenv("OPENPYPE_DONT_VERIFY_SSL",
|
||||
True) else True # noqa
|
||||
|
||||
auth = kwargs.get("auth")
|
||||
if auth:
|
||||
kwargs["auth"] = tuple(auth) # explicit cast to tuple
|
||||
|
|
@ -61,8 +53,8 @@ def requests_post(*args, **kwargs):
|
|||
def requests_get(*args, **kwargs):
|
||||
"""Wrap request get method.
|
||||
|
||||
Disabling SSL certificate validation if ``DONT_VERIFY_SSL`` environment
|
||||
variable is found. This is useful when Deadline server is
|
||||
Disabling SSL certificate validation if ``verify`` kwarg is set to False.
|
||||
This is useful when Deadline server is
|
||||
running with self-signed certificates and its certificate is not
|
||||
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.
|
||||
|
||||
"""
|
||||
if 'verify' not in kwargs:
|
||||
kwargs['verify'] = False if os.getenv("OPENPYPE_DONT_VERIFY_SSL",
|
||||
True) else True # noqa
|
||||
auth = kwargs.get("auth")
|
||||
if auth:
|
||||
kwargs["auth"] = tuple(auth)
|
||||
|
|
@ -466,7 +455,8 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin,
|
|||
self.aux_files = self.get_aux_files()
|
||||
|
||||
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))
|
||||
|
||||
# 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,
|
||||
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)
|
||||
|
||||
def process_submission(self, auth=None):
|
||||
def process_submission(self, auth=None, verify=True):
|
||||
"""Process data for submission.
|
||||
|
||||
This takes Deadline JobInfo, PluginInfo, AuxFile, creates payload
|
||||
|
|
@ -493,7 +483,7 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin,
|
|||
|
||||
"""
|
||||
payload = self.assemble_payload()
|
||||
return self.submit(payload, auth)
|
||||
return self.submit(payload, auth, verify)
|
||||
|
||||
@abstractmethod
|
||||
def get_job_info(self):
|
||||
|
|
@ -583,7 +573,7 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin,
|
|||
"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.
|
||||
|
||||
This takes payload in the form of JSON file and POST it to
|
||||
|
|
@ -592,6 +582,7 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin,
|
|||
Args:
|
||||
payload (dict): dict to become json in deadline submission.
|
||||
auth (tuple): (username, password)
|
||||
verify (bool): verify SSL certificate if present
|
||||
|
||||
Returns:
|
||||
str: resulting Deadline job id.
|
||||
|
|
@ -601,8 +592,8 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin,
|
|||
|
||||
"""
|
||||
url = "{}/api/jobs".format(self._deadline_url)
|
||||
response = requests_post(url, json=payload,
|
||||
auth=auth)
|
||||
response = requests_post(
|
||||
url, json=payload, auth=auth, verify=verify)
|
||||
if not response.ok:
|
||||
self.log.error("Submission failed!")
|
||||
self.log.error(response.status_code)
|
||||
|
|
|
|||
|
|
@ -26,27 +26,32 @@ class CollectDeadlinePools(pyblish.api.InstancePlugin,
|
|||
|
||||
order = pyblish.api.CollectorOrder + 0.420
|
||||
label = "Collect Deadline Pools"
|
||||
hosts = ["aftereffects",
|
||||
"fusion",
|
||||
"harmony"
|
||||
"nuke",
|
||||
"maya",
|
||||
"max",
|
||||
"houdini"]
|
||||
hosts = [
|
||||
"aftereffects",
|
||||
"fusion",
|
||||
"harmony",
|
||||
"maya",
|
||||
"max",
|
||||
"houdini",
|
||||
"nuke",
|
||||
]
|
||||
|
||||
families = ["render",
|
||||
"rendering",
|
||||
"render.farm",
|
||||
"renderFarm",
|
||||
"renderlayer",
|
||||
"maxrender",
|
||||
"usdrender",
|
||||
"redshift_rop",
|
||||
"arnold_rop",
|
||||
"mantra_rop",
|
||||
"karma_rop",
|
||||
"vray_rop",
|
||||
"publish.hou"]
|
||||
families = [
|
||||
"render",
|
||||
"prerender",
|
||||
"rendering",
|
||||
"render.farm",
|
||||
"renderFarm",
|
||||
"renderlayer",
|
||||
"maxrender",
|
||||
"usdrender",
|
||||
"redshift_rop",
|
||||
"arnold_rop",
|
||||
"mantra_rop",
|
||||
"karma_rop",
|
||||
"vray_rop",
|
||||
"publish.hou",
|
||||
]
|
||||
|
||||
primary_pool = None
|
||||
secondary_pool = None
|
||||
|
|
|
|||
|
|
@ -76,6 +76,9 @@ class CollectDeadlineUserCredentials(pyblish.api.InstancePlugin):
|
|||
)
|
||||
instance.data["deadline"]["auth"] = None
|
||||
|
||||
instance.data["deadline"]["verify"] = (
|
||||
not deadline_info["not_verify_ssl"])
|
||||
|
||||
if not deadline_info["require_authentication"]:
|
||||
return
|
||||
# TODO import 'get_addon_site_settings' when available
|
||||
|
|
|
|||
|
|
@ -174,8 +174,9 @@ class BlenderSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline,
|
|||
instance.data["toBeRenderedOn"] = "deadline"
|
||||
|
||||
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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -193,9 +193,11 @@ class CelactionSubmitDeadline(pyblish.api.InstancePlugin):
|
|||
self.expected_files(instance, render_path)
|
||||
self.log.debug("__ expectedFiles: `{}`".format(
|
||||
instance.data["expectedFiles"]))
|
||||
|
||||
auth = instance.data["deadline"]["auth"]
|
||||
verify = instance.data["deadline"]["verify"]
|
||||
response = requests_post(self.deadline_url, json=payload,
|
||||
auth=instance.data["deadline"]["require_authentication"])
|
||||
auth=auth,
|
||||
verify=verify)
|
||||
|
||||
if not response.ok:
|
||||
self.log.error(
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ class FusionSubmitDeadline(
|
|||
else:
|
||||
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"]
|
||||
assert deadline_url, "Requires Deadline Webservice URL"
|
||||
|
|
@ -242,7 +242,8 @@ class FusionSubmitDeadline(
|
|||
# E.g. http://192.168.0.1:8082/api/jobs
|
||||
url = "{}/api/jobs".format(deadline_url)
|
||||
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:
|
||||
raise Exception(response.text)
|
||||
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ class HoudiniSubmitDeadline(
|
|||
priority = 50
|
||||
chunk_size = 1
|
||||
group = ""
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_attribute_defs(cls):
|
||||
return [
|
||||
|
|
@ -188,7 +188,7 @@ class HoudiniSubmitDeadline(
|
|||
|
||||
job_info.Pool = instance.data.get("primaryPool")
|
||||
job_info.SecondaryPool = instance.data.get("secondaryPool")
|
||||
|
||||
|
||||
if split_render_job and is_export_job:
|
||||
job_info.Priority = attribute_values.get(
|
||||
"export_priority", self.export_priority
|
||||
|
|
@ -309,6 +309,11 @@ class HoudiniSubmitDeadline(
|
|||
return attr.asdict(plugin_info)
|
||||
|
||||
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)
|
||||
|
||||
# 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
|
||||
)
|
||||
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_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.abstract_submit_deadline import DeadlineJobInfo
|
||||
|
||||
|
|
@ -181,27 +181,35 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline,
|
|||
|
||||
self.log.debug("Submitting 3dsMax render..")
|
||||
project_settings = instance.context.data["project_settings"]
|
||||
auth = instance.data["deadline"]["auth"]
|
||||
verify = instance.data["deadline"]["verify"]
|
||||
if instance.data.get("multiCamera"):
|
||||
self.log.debug("Submitting jobs for multiple cameras..")
|
||||
payload = self._use_published_name_for_multiples(
|
||||
payload_data, project_settings)
|
||||
job_infos, plugin_infos = payload
|
||||
for job_info, plugin_info in zip(job_infos, plugin_infos):
|
||||
self.submit(self.assemble_payload(job_info, plugin_info),
|
||||
instance.data["deadline"]["auth"])
|
||||
self.submit(
|
||||
self.assemble_payload(job_info, plugin_info),
|
||||
auth=auth,
|
||||
verify=verify
|
||||
)
|
||||
else:
|
||||
payload = self._use_published_name(payload_data, project_settings)
|
||||
job_info, plugin_info = payload
|
||||
self.submit(self.assemble_payload(job_info, plugin_info),
|
||||
instance.data["deadline"]["auth"])
|
||||
self.submit(
|
||||
self.assemble_payload(job_info, plugin_info),
|
||||
auth=auth,
|
||||
verify=verify
|
||||
)
|
||||
|
||||
def _use_published_name(self, data, project_settings):
|
||||
# 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_multipass_setting
|
||||
)
|
||||
from ayon_core.hosts.max.api.lib_rendersettings import RenderSettings
|
||||
from ayon_max.api.lib_rendersettings import RenderSettings
|
||||
|
||||
instance = self._instance
|
||||
job_info = copy.deepcopy(self.job_info)
|
||||
|
|
|
|||
|
|
@ -39,8 +39,8 @@ from ayon_core.lib import (
|
|||
EnumDef,
|
||||
is_in_tests,
|
||||
)
|
||||
from ayon_core.hosts.maya.api.lib_rendersettings import RenderSettings
|
||||
from ayon_core.hosts.maya.api.lib import get_attr_in_layer
|
||||
from ayon_maya.api.lib_rendersettings import RenderSettings
|
||||
from ayon_maya.api.lib import get_attr_in_layer
|
||||
|
||||
from openpype_modules.deadline import abstract_submit_deadline
|
||||
from openpype_modules.deadline.abstract_submit_deadline import DeadlineJobInfo
|
||||
|
|
@ -292,7 +292,7 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline,
|
|||
|
||||
return plugin_payload
|
||||
|
||||
def process_submission(self, auth=None):
|
||||
def process_submission(self, auth=None, verify=True):
|
||||
from maya import cmds
|
||||
instance = self._instance
|
||||
|
||||
|
|
@ -332,8 +332,10 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline,
|
|||
if "vrayscene" in instance.data["families"]:
|
||||
self.log.debug("Submitting V-Ray scene render..")
|
||||
vray_export_payload = self._get_vray_export_payload(payload_data)
|
||||
|
||||
export_job = self.submit(vray_export_payload,
|
||||
instance.data["deadline"]["auth"])
|
||||
auth=auth,
|
||||
verify=verify)
|
||||
|
||||
payload = self._get_vray_render_payload(payload_data)
|
||||
|
||||
|
|
@ -353,7 +355,8 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline,
|
|||
# Submit main render job
|
||||
job_info, plugin_info = payload
|
||||
self.submit(self.assemble_payload(job_info, plugin_info),
|
||||
instance.data["deadline"]["auth"])
|
||||
auth=auth,
|
||||
verify=verify)
|
||||
|
||||
def _tile_render(self, payload):
|
||||
"""Submit as tile render per frame with dependent assembly jobs."""
|
||||
|
|
@ -557,13 +560,18 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline,
|
|||
# Submit assembly jobs
|
||||
assembly_job_ids = []
|
||||
num_assemblies = len(assembly_payloads)
|
||||
auth = instance.data["deadline"]["auth"]
|
||||
verify = instance.data["deadline"]["verify"]
|
||||
for i, payload in enumerate(assembly_payloads):
|
||||
self.log.debug(
|
||||
"submitting assembly job {} of {}".format(i + 1,
|
||||
num_assemblies)
|
||||
)
|
||||
assembly_job_id = self.submit(payload,
|
||||
instance.data["deadline"]["auth"])
|
||||
assembly_job_id = self.submit(
|
||||
payload,
|
||||
auth=auth,
|
||||
verify=verify
|
||||
)
|
||||
assembly_job_ids.append(assembly_job_id)
|
||||
|
||||
instance.data["assemblySubmissionJobs"] = assembly_job_ids
|
||||
|
|
|
|||
|
|
@ -424,8 +424,12 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin,
|
|||
self.log.debug("__ expectedFiles: `{}`".format(
|
||||
instance.data["expectedFiles"]))
|
||||
auth = instance.data["deadline"]["auth"]
|
||||
response = requests_post(self.deadline_url, json=payload, timeout=10,
|
||||
auth=auth)
|
||||
verify = instance.data["deadline"]["verify"]
|
||||
response = requests_post(self.deadline_url,
|
||||
json=payload,
|
||||
timeout=10,
|
||||
auth=auth,
|
||||
verify=verify)
|
||||
|
||||
if not response.ok:
|
||||
raise Exception(response.text)
|
||||
|
|
|
|||
|
|
@ -210,8 +210,9 @@ class ProcessSubmittedCacheJobOnFarm(pyblish.api.InstancePlugin,
|
|||
|
||||
url = "{}/api/jobs".format(self.deadline_url)
|
||||
auth = instance.data["deadline"]["auth"]
|
||||
response = requests_post(url, json=payload, timeout=10,
|
||||
auth=auth)
|
||||
verify = instance.data["deadline"]["verify"]
|
||||
response = requests_post(
|
||||
url, json=payload, timeout=10, auth=auth, verify=verify)
|
||||
if not response.ok:
|
||||
raise Exception(response.text)
|
||||
|
||||
|
|
|
|||
|
|
@ -304,8 +304,9 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin,
|
|||
|
||||
url = "{}/api/jobs".format(self.deadline_url)
|
||||
auth = instance.data["deadline"]["auth"]
|
||||
response = requests_post(url, json=payload, timeout=10,
|
||||
auth=auth)
|
||||
verify = instance.data["deadline"]["verify"]
|
||||
response = requests_post(
|
||||
url, json=payload, timeout=10, auth=auth, verify=verify)
|
||||
if not response.ok:
|
||||
raise Exception(response.text)
|
||||
|
||||
|
|
@ -467,8 +468,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin,
|
|||
|
||||
# Inject deadline url to instances to query DL for job id for overrides
|
||||
for inst in instances:
|
||||
if not "deadline" in inst:
|
||||
inst["deadline"] = {}
|
||||
inst["deadline"] = instance.data["deadline"]
|
||||
|
||||
# publish job file
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ class ValidateDeadlinePools(OptionalPyblishPluginMixin,
|
|||
auth=auth,
|
||||
log=self.log)
|
||||
# some DL return "none" as a pool name
|
||||
if not "none" in pools:
|
||||
if "none" not in pools:
|
||||
pools.append("none")
|
||||
self.log.info("Available pools: {}".format(pools))
|
||||
self.pools_per_url[deadline_url] = pools
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
__version__ = "0.1.10"
|
||||
__version__ = "0.1.12"
|
||||
|
|
|
|||
|
|
@ -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 .rr_job import SubmitFile
|
||||
from .rr_job import RRjob, SubmitterParameter # noqa F401
|
||||
from .rr_job import RRJob, SubmitterParameter # noqa F401
|
||||
|
||||
|
||||
class Api:
|
||||
|
|
|
|||
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):
|
||||
"""Prepare template data for current context.
|
||||
|
||||
|
|
|
|||
|
|
@ -681,7 +681,7 @@ class PublishAttributeValues(AttributeValues):
|
|||
|
||||
@property
|
||||
def parent(self):
|
||||
self.publish_attributes.parent
|
||||
return self.publish_attributes.parent
|
||||
|
||||
|
||||
class PublishAttributes:
|
||||
|
|
@ -2053,7 +2053,7 @@ class CreateContext:
|
|||
exc_info = sys.exc_info()
|
||||
self.log.warning(error_message.format(identifier, exc_info[1]))
|
||||
|
||||
except:
|
||||
except: # noqa: E722
|
||||
add_traceback = True
|
||||
exc_info = sys.exc_info()
|
||||
self.log.warning(
|
||||
|
|
@ -2163,7 +2163,7 @@ class CreateContext:
|
|||
exc_info = sys.exc_info()
|
||||
self.log.warning(error_message.format(identifier, exc_info[1]))
|
||||
|
||||
except:
|
||||
except: # noqa: E722
|
||||
failed = True
|
||||
add_traceback = True
|
||||
exc_info = sys.exc_info()
|
||||
|
|
@ -2197,7 +2197,7 @@ class CreateContext:
|
|||
try:
|
||||
convertor.find_instances()
|
||||
|
||||
except:
|
||||
except: # noqa: E722
|
||||
failed_info.append(
|
||||
prepare_failed_convertor_operation_info(
|
||||
convertor.identifier, sys.exc_info()
|
||||
|
|
@ -2373,7 +2373,7 @@ class CreateContext:
|
|||
exc_info = sys.exc_info()
|
||||
self.log.warning(error_message.format(identifier, exc_info[1]))
|
||||
|
||||
except:
|
||||
except: # noqa: E722
|
||||
failed = True
|
||||
add_traceback = True
|
||||
exc_info = sys.exc_info()
|
||||
|
|
@ -2440,7 +2440,7 @@ class CreateContext:
|
|||
error_message.format(identifier, exc_info[1])
|
||||
)
|
||||
|
||||
except:
|
||||
except: # noqa: E722
|
||||
failed = True
|
||||
add_traceback = True
|
||||
exc_info = sys.exc_info()
|
||||
|
|
@ -2546,7 +2546,7 @@ class CreateContext:
|
|||
try:
|
||||
self.run_convertor(convertor_identifier)
|
||||
|
||||
except:
|
||||
except: # noqa: E722
|
||||
failed_info.append(
|
||||
prepare_failed_convertor_operation_info(
|
||||
convertor_identifier, sys.exc_info()
|
||||
|
|
|
|||
|
|
@ -73,8 +73,8 @@ def get_folder_template_data(folder_entity, project_name):
|
|||
- 'parent' - direct parent name, project name used if is under
|
||||
project
|
||||
|
||||
Required document fields:
|
||||
Folder: 'path' -> Plan to require: 'folderType'
|
||||
Required entity fields:
|
||||
Folder: 'path', 'folderType'
|
||||
|
||||
Args:
|
||||
folder_entity (Dict[str, Any]): Folder entity.
|
||||
|
|
@ -101,6 +101,8 @@ def get_folder_template_data(folder_entity, project_name):
|
|||
return {
|
||||
"folder": {
|
||||
"name": folder_name,
|
||||
"type": folder_entity["folderType"],
|
||||
"path": path,
|
||||
},
|
||||
"asset": folder_name,
|
||||
"hierarchy": hierarchy,
|
||||
|
|
|
|||
263
client/ayon_core/pipeline/thumbnails.py
Normal file
263
client/ayon_core/pipeline/thumbnails.py
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
import os
|
||||
import time
|
||||
import collections
|
||||
|
||||
import ayon_api
|
||||
|
||||
from ayon_core.lib.local_settings import get_ayon_appdirs
|
||||
|
||||
|
||||
FileInfo = collections.namedtuple(
|
||||
"FileInfo",
|
||||
("path", "size", "modification_time")
|
||||
)
|
||||
|
||||
|
||||
class ThumbnailsCache:
|
||||
"""Cache of thumbnails on local storage.
|
||||
|
||||
Thumbnails are cached to appdirs to predefined directory. Each project has
|
||||
own subfolder with thumbnails -> that's because each project has own
|
||||
thumbnail id validation and file names are thumbnail ids with matching
|
||||
extension. Extensions are predefined (.png and .jpeg).
|
||||
|
||||
Cache has cleanup mechanism which is triggered on initialized by default.
|
||||
|
||||
The cleanup has 2 levels:
|
||||
1. soft cleanup which remove all files that are older then 'days_alive'
|
||||
2. max size cleanup which remove all files until the thumbnails folder
|
||||
contains less then 'max_filesize'
|
||||
- this is time consuming so it's not triggered automatically
|
||||
|
||||
Args:
|
||||
cleanup (bool): Trigger soft cleanup (Cleanup expired thumbnails).
|
||||
"""
|
||||
|
||||
# Lifetime of thumbnails (in seconds)
|
||||
# - default 3 days
|
||||
days_alive = 3
|
||||
# Max size of thumbnail directory (in bytes)
|
||||
# - default 2 Gb
|
||||
max_filesize = 2 * 1024 * 1024 * 1024
|
||||
|
||||
def __init__(self, cleanup=True):
|
||||
self._thumbnails_dir = None
|
||||
self._days_alive_secs = self.days_alive * 24 * 60 * 60
|
||||
if cleanup:
|
||||
self.cleanup()
|
||||
|
||||
def get_thumbnails_dir(self):
|
||||
"""Root directory where thumbnails are stored.
|
||||
|
||||
Returns:
|
||||
str: Path to thumbnails root.
|
||||
"""
|
||||
|
||||
if self._thumbnails_dir is None:
|
||||
self._thumbnails_dir = get_ayon_appdirs("thumbnails")
|
||||
return self._thumbnails_dir
|
||||
|
||||
thumbnails_dir = property(get_thumbnails_dir)
|
||||
|
||||
def get_thumbnails_dir_file_info(self):
|
||||
"""Get information about all files in thumbnails directory.
|
||||
|
||||
Returns:
|
||||
List[FileInfo]: List of file information about all files.
|
||||
"""
|
||||
|
||||
thumbnails_dir = self.thumbnails_dir
|
||||
files_info = []
|
||||
if not os.path.exists(thumbnails_dir):
|
||||
return files_info
|
||||
|
||||
for root, _, filenames in os.walk(thumbnails_dir):
|
||||
for filename in filenames:
|
||||
path = os.path.join(root, filename)
|
||||
files_info.append(FileInfo(
|
||||
path, os.path.getsize(path), os.path.getmtime(path)
|
||||
))
|
||||
return files_info
|
||||
|
||||
def get_thumbnails_dir_size(self, files_info=None):
|
||||
"""Got full size of thumbnail directory.
|
||||
|
||||
Args:
|
||||
files_info (List[FileInfo]): Prepared file information about
|
||||
files in thumbnail directory.
|
||||
|
||||
Returns:
|
||||
int: File size of all files in thumbnail directory.
|
||||
"""
|
||||
|
||||
if files_info is None:
|
||||
files_info = self.get_thumbnails_dir_file_info()
|
||||
|
||||
if not files_info:
|
||||
return 0
|
||||
|
||||
return sum(
|
||||
file_info.size
|
||||
for file_info in files_info
|
||||
)
|
||||
|
||||
def cleanup(self, check_max_size=False):
|
||||
"""Cleanup thumbnails directory.
|
||||
|
||||
Args:
|
||||
check_max_size (bool): Also cleanup files to match max size of
|
||||
thumbnails directory.
|
||||
"""
|
||||
|
||||
thumbnails_dir = self.get_thumbnails_dir()
|
||||
# Skip if thumbnails dir does not exist yet
|
||||
if not os.path.exists(thumbnails_dir):
|
||||
return
|
||||
|
||||
self._soft_cleanup(thumbnails_dir)
|
||||
if check_max_size:
|
||||
self._max_size_cleanup(thumbnails_dir)
|
||||
|
||||
def _soft_cleanup(self, thumbnails_dir):
|
||||
current_time = time.time()
|
||||
for root, _, filenames in os.walk(thumbnails_dir):
|
||||
for filename in filenames:
|
||||
path = os.path.join(root, filename)
|
||||
modification_time = os.path.getmtime(path)
|
||||
if current_time - modification_time > self._days_alive_secs:
|
||||
os.remove(path)
|
||||
|
||||
def _max_size_cleanup(self, thumbnails_dir):
|
||||
files_info = self.get_thumbnails_dir_file_info()
|
||||
size = self.get_thumbnails_dir_size(files_info)
|
||||
if size < self.max_filesize:
|
||||
return
|
||||
|
||||
sorted_file_info = collections.deque(
|
||||
sorted(files_info, key=lambda item: item.modification_time)
|
||||
)
|
||||
diff = size - self.max_filesize
|
||||
while diff > 0:
|
||||
if not sorted_file_info:
|
||||
break
|
||||
|
||||
file_info = sorted_file_info.popleft()
|
||||
diff -= file_info.size
|
||||
os.remove(file_info.path)
|
||||
|
||||
def get_thumbnail_filepath(self, project_name, thumbnail_id):
|
||||
"""Get thumbnail by thumbnail id.
|
||||
|
||||
Args:
|
||||
project_name (str): Name of project.
|
||||
thumbnail_id (str): Thumbnail id.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Path to thumbnail image or None if thumbnail
|
||||
is not cached yet.
|
||||
"""
|
||||
|
||||
if not thumbnail_id:
|
||||
return None
|
||||
|
||||
for ext in (
|
||||
".png",
|
||||
".jpeg",
|
||||
):
|
||||
filepath = os.path.join(
|
||||
self.thumbnails_dir, project_name, thumbnail_id + ext
|
||||
)
|
||||
if os.path.exists(filepath):
|
||||
return filepath
|
||||
return None
|
||||
|
||||
def get_project_dir(self, project_name):
|
||||
"""Path to root directory for specific project.
|
||||
|
||||
Args:
|
||||
project_name (str): Name of project for which root directory path
|
||||
should be returned.
|
||||
|
||||
Returns:
|
||||
str: Path to root of project's thumbnails.
|
||||
"""
|
||||
|
||||
return os.path.join(self.thumbnails_dir, project_name)
|
||||
|
||||
def make_sure_project_dir_exists(self, project_name):
|
||||
project_dir = self.get_project_dir(project_name)
|
||||
if not os.path.exists(project_dir):
|
||||
os.makedirs(project_dir)
|
||||
return project_dir
|
||||
|
||||
def store_thumbnail(self, project_name, thumbnail_id, content, mime_type):
|
||||
"""Store thumbnail to cache folder.
|
||||
|
||||
Args:
|
||||
project_name (str): Project where the thumbnail belong to.
|
||||
thumbnail_id (str): Thumbnail id.
|
||||
content (bytes): Byte content of thumbnail file.
|
||||
mime_type (str): Type of content.
|
||||
|
||||
Returns:
|
||||
str: Path to cached thumbnail image file.
|
||||
"""
|
||||
|
||||
if mime_type == "image/png":
|
||||
ext = ".png"
|
||||
elif mime_type == "image/jpeg":
|
||||
ext = ".jpeg"
|
||||
else:
|
||||
raise ValueError(
|
||||
"Unknown mime type for thumbnail \"{}\"".format(mime_type))
|
||||
|
||||
project_dir = self.make_sure_project_dir_exists(project_name)
|
||||
thumbnail_path = os.path.join(project_dir, thumbnail_id + ext)
|
||||
with open(thumbnail_path, "wb") as stream:
|
||||
stream.write(content)
|
||||
|
||||
current_time = time.time()
|
||||
os.utime(thumbnail_path, (current_time, current_time))
|
||||
|
||||
return thumbnail_path
|
||||
|
||||
|
||||
class _CacheItems:
|
||||
thumbnails_cache = ThumbnailsCache()
|
||||
|
||||
|
||||
def get_thumbnail_path(project_name, thumbnail_id):
|
||||
"""Get path to thumbnail image.
|
||||
|
||||
Args:
|
||||
project_name (str): Project where thumbnail belongs to.
|
||||
thumbnail_id (Union[str, None]): Thumbnail id.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Path to thumbnail image or None if thumbnail
|
||||
id is not valid or thumbnail was not possible to receive.
|
||||
|
||||
"""
|
||||
if not thumbnail_id:
|
||||
return None
|
||||
|
||||
filepath = _CacheItems.thumbnails_cache.get_thumbnail_filepath(
|
||||
project_name, thumbnail_id
|
||||
)
|
||||
if filepath is not None:
|
||||
return filepath
|
||||
|
||||
# 'ayon_api' had a bug, public function
|
||||
# 'get_thumbnail_by_id' did not return output of
|
||||
# 'ServerAPI' method.
|
||||
con = ayon_api.get_server_api_connection()
|
||||
result = con.get_thumbnail_by_id(project_name, thumbnail_id)
|
||||
|
||||
if result is not None and result.is_valid:
|
||||
return _CacheItems.thumbnails_cache.store_thumbnail(
|
||||
project_name,
|
||||
thumbnail_id,
|
||||
result.content,
|
||||
result.content_type
|
||||
)
|
||||
return None
|
||||
|
|
@ -33,6 +33,7 @@ import collections
|
|||
import pyblish.api
|
||||
import ayon_api
|
||||
|
||||
from ayon_core.pipeline.template_data import get_folder_template_data
|
||||
from ayon_core.pipeline.version_start import get_versioning_start
|
||||
|
||||
|
||||
|
|
@ -383,24 +384,11 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin):
|
|||
# - 'folder', 'hierarchy', 'parent', 'folder'
|
||||
folder_entity = instance.data.get("folderEntity")
|
||||
if folder_entity:
|
||||
folder_name = folder_entity["name"]
|
||||
folder_path = folder_entity["path"]
|
||||
hierarchy_parts = folder_path.split("/")
|
||||
hierarchy_parts.pop(0)
|
||||
hierarchy_parts.pop(-1)
|
||||
parent_name = project_entity["name"]
|
||||
if hierarchy_parts:
|
||||
parent_name = hierarchy_parts[-1]
|
||||
|
||||
hierarchy = "/".join(hierarchy_parts)
|
||||
anatomy_data.update({
|
||||
"asset": folder_name,
|
||||
"hierarchy": hierarchy,
|
||||
"parent": parent_name,
|
||||
"folder": {
|
||||
"name": folder_name,
|
||||
},
|
||||
})
|
||||
folder_data = get_folder_template_data(
|
||||
folder_entity,
|
||||
project_entity["name"]
|
||||
)
|
||||
anatomy_data.update(folder_data)
|
||||
return
|
||||
|
||||
if instance.data.get("newAssetPublishing"):
|
||||
|
|
@ -418,6 +406,11 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin):
|
|||
"parent": parent_name,
|
||||
"folder": {
|
||||
"name": folder_name,
|
||||
"path": instance.data["folderPath"],
|
||||
# TODO get folder type from hierarchy
|
||||
# Using 'Shot' is current default behavior of editorial
|
||||
# (or 'newAssetPublishing') publishing.
|
||||
"type": "Shot",
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -108,69 +108,6 @@ class IntegrateAsset(pyblish.api.InstancePlugin):
|
|||
|
||||
label = "Integrate Asset"
|
||||
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"
|
||||
|
||||
|
|
@ -360,7 +297,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin):
|
|||
|
||||
# Compute the resource file infos once (files belonging to the
|
||||
# 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_destinations, anatomy
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
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 import (
|
||||
get_current_host_name,
|
||||
registered_host,
|
||||
PublishXmlValidationError,
|
||||
OptionalPyblishPluginMixin
|
||||
)
|
||||
|
|
@ -18,17 +23,50 @@ class ShowInventory(pyblish.api.Action):
|
|||
host_tools.show_scene_inventory()
|
||||
|
||||
|
||||
class ValidateContainers(OptionalPyblishPluginMixin,
|
||||
pyblish.api.ContextPlugin):
|
||||
|
||||
class ValidateOutdatedContainers(
|
||||
OptionalPyblishPluginMixin,
|
||||
pyblish.api.ContextPlugin
|
||||
):
|
||||
"""Containers are must be updated to latest version on publish."""
|
||||
|
||||
label = "Validate Outdated Containers"
|
||||
order = pyblish.api.ValidatorOrder
|
||||
hosts = ["maya", "houdini", "nuke", "harmony", "photoshop", "aftereffects"]
|
||||
|
||||
optional = True
|
||||
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):
|
||||
if not self.is_active(context.data):
|
||||
return
|
||||
|
|
|
|||
|
|
@ -1,8 +1,14 @@
|
|||
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.
|
||||
|
||||
AYON does not allow overwriting previously published versions.
|
||||
|
|
@ -11,13 +17,39 @@ class ValidateVersion(pyblish.api.InstancePlugin):
|
|||
order = pyblish.api.ValidatorOrder
|
||||
|
||||
label = "Validate Version"
|
||||
hosts = ["nuke", "maya", "houdini", "blender",
|
||||
"photoshop", "aftereffects"]
|
||||
|
||||
optional = False
|
||||
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):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
version = instance.data.get("version")
|
||||
latest_version = instance.data.get("latestVersion")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,28 +1,31 @@
|
|||
"""OpenColorIO Wrapper.
|
||||
|
||||
Only to be interpreted by Python 3. It is run in subprocess in case
|
||||
Python 2 hosts needs to use it. Or it is used as module for Python 3
|
||||
processing.
|
||||
|
||||
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.
|
||||
Receive OpenColorIO information and store it in JSON format for processed
|
||||
that don't have access to OpenColorIO or their version of OpenColorIO is
|
||||
not compatible.
|
||||
"""
|
||||
|
||||
import click
|
||||
import json
|
||||
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()
|
||||
|
|
@ -30,404 +33,185 @@ def main():
|
|||
pass # noqa: WPS100
|
||||
|
||||
|
||||
@main.group()
|
||||
def config():
|
||||
"""Config related commands group
|
||||
|
||||
Example of use:
|
||||
> pyton.exe ./ocio_wrapper.py config <command> *args
|
||||
"""
|
||||
pass # noqa: WPS100
|
||||
|
||||
|
||||
@main.group()
|
||||
def colorspace():
|
||||
"""Colorspace related commands group
|
||||
|
||||
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):
|
||||
@main.command(
|
||||
name="get_ocio_config_colorspaces",
|
||||
help="return all colorspaces 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_ocio_config_colorspaces(config_path, output_path):
|
||||
"""Aggregate all colorspace to file.
|
||||
|
||||
Python 2 wrapped console command
|
||||
|
||||
Args:
|
||||
in_path (str): config file path string
|
||||
out_path (str): temp json file path string
|
||||
config_path (str): config file path string
|
||||
output_path (str): temp json file path string
|
||||
|
||||
Example of use:
|
||||
> 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)
|
||||
|
||||
out_data = _get_colorspace_data(in_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"
|
||||
_save_output_to_json_file(
|
||||
get_ocio_config_colorspaces(config_path),
|
||||
output_path
|
||||
)
|
||||
)
|
||||
@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_views(in_path, out_path):
|
||||
|
||||
|
||||
@main.command(
|
||||
name="get_ocio_config_views",
|
||||
help="All viewers 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_ocio_config_views(config_path, output_path):
|
||||
"""Aggregate all viewers to file.
|
||||
|
||||
Python 2 wrapped console command
|
||||
|
||||
Args:
|
||||
in_path (str): config file path string
|
||||
out_path (str): temp json file path string
|
||||
config_path (str): config file path string
|
||||
output_path (str): temp json file path string
|
||||
|
||||
Example of use:
|
||||
> pyton.exe ./ocio_wrapper.py config get_views \
|
||||
--in_path=<path> --out_path=<path>
|
||||
--config_path <path> --output <path>
|
||||
"""
|
||||
json_path = Path(out_path)
|
||||
|
||||
out_data = _get_views_data(in_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"
|
||||
_save_output_to_json_file(
|
||||
get_ocio_config_views(config_path),
|
||||
output_path
|
||||
)
|
||||
)
|
||||
@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:
|
||||
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:
|
||||
> 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)
|
||||
|
||||
out_data = _get_version_data(config_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)"
|
||||
_save_output_to_json_file(
|
||||
get_config_version_data(config_path),
|
||||
output_path
|
||||
)
|
||||
)
|
||||
@click.option("--config_path", required=True,
|
||||
help="path where to read ocio config file",
|
||||
type=click.Path(exists=True))
|
||||
@click.option("--filepath", required=True,
|
||||
help="path to file to get colorspace from",
|
||||
type=click.Path())
|
||||
@click.option("--out_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, out_path
|
||||
|
||||
|
||||
@main.command(
|
||||
name="get_config_file_rules_colorspace_from_filepath",
|
||||
help="Colorspace file rules from filepath")
|
||||
@click.option(
|
||||
"--config_path",
|
||||
required=True,
|
||||
help="OCIO config path to read ocio config file.",
|
||||
type=click.Path(exists=True))
|
||||
@click.option(
|
||||
"--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.
|
||||
|
||||
Python 2 wrapped console command
|
||||
|
||||
Args:
|
||||
config_path (str): config file path string
|
||||
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:
|
||||
> pyton.exe ./ocio_wrapper.py \
|
||||
> python.exe ./ocio_wrapper.py \
|
||||
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)
|
||||
|
||||
colorspace = _get_config_file_rules_colorspace_from_filepath(
|
||||
config_path, filepath)
|
||||
|
||||
with open(json_path, "w") as f_:
|
||||
json.dump(colorspace, f_)
|
||||
|
||||
print(f"Colorspace name is saved to '{json_path}'")
|
||||
_save_output_to_json_file(
|
||||
get_config_file_rules_colorspace_from_filepath(config_path, filepath),
|
||||
output_path
|
||||
)
|
||||
|
||||
|
||||
def _get_config_file_rules_colorspace_from_filepath(config_path, filepath):
|
||||
"""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(
|
||||
@main.command(
|
||||
name="get_display_view_colorspace_name",
|
||||
help=(
|
||||
"return default view colorspace name "
|
||||
"for the given display and view "
|
||||
"--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())
|
||||
@click.option("--display", required=True,
|
||||
help="display name",
|
||||
type=click.STRING)
|
||||
@click.option("--view", required=True,
|
||||
help="view name",
|
||||
type=click.STRING)
|
||||
def get_display_view_colorspace_name(in_path, out_path,
|
||||
display, view):
|
||||
"Default view colorspace name for the given display and view"
|
||||
))
|
||||
@click.option(
|
||||
"--config_path",
|
||||
required=True,
|
||||
help="path where to read ocio config file",
|
||||
type=click.Path(exists=True))
|
||||
@click.option(
|
||||
"--display",
|
||||
required=True,
|
||||
help="Display name",
|
||||
type=click.STRING)
|
||||
@click.option(
|
||||
"--view",
|
||||
required=True,
|
||||
help="view name",
|
||||
type=click.STRING)
|
||||
@click.option(
|
||||
"--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.
|
||||
|
||||
Wrapper command for processes without access to OpenColorIO
|
||||
|
||||
Args:
|
||||
in_path (str): config file path string
|
||||
out_path (str): temp json file path string
|
||||
config_path (str): config file path string
|
||||
output_path (str): temp json file path string
|
||||
display (str): display name e.g. "ACES"
|
||||
view (str): view name e.g. "sRGB"
|
||||
|
||||
Example of use:
|
||||
> pyton.exe ./ocio_wrapper.py config \
|
||||
get_display_view_colorspace_name --in_path=<path> \
|
||||
--out_path=<path> --display=<display> --view=<view>
|
||||
get_display_view_colorspace_name --config_path <path> \
|
||||
--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:
|
||||
json.dump(out_data, f)
|
||||
|
||||
print(f"Display view colorspace saved to '{out_path}'")
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
if not has_compatible_ocio_package():
|
||||
raise RuntimeError("OpenColorIO is not available.")
|
||||
main()
|
||||
|
|
|
|||
|
|
@ -104,14 +104,11 @@ class WebServerTool:
|
|||
again. In that case, use existing running webserver.
|
||||
Check here is easier than capturing exception from thread.
|
||||
"""
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
result = True
|
||||
try:
|
||||
sock.bind((host_name, port))
|
||||
result = False
|
||||
except:
|
||||
print("Port is in use")
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as con:
|
||||
result = con.connect_ex((host_name, port)) == 0
|
||||
|
||||
if result:
|
||||
print(f"Port {port} is already in use")
|
||||
return result
|
||||
|
||||
def call(self, func):
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ from .hierarchy import (
|
|||
)
|
||||
from .thumbnails import ThumbnailsModel
|
||||
from .selection import HierarchyExpectedSelection
|
||||
from .users import UsersModel
|
||||
|
||||
|
||||
__all__ = (
|
||||
|
|
@ -32,4 +33,6 @@ __all__ = (
|
|||
"ThumbnailsModel",
|
||||
|
||||
"HierarchyExpectedSelection",
|
||||
|
||||
"UsersModel",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import ayon_api
|
|||
import six
|
||||
|
||||
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"
|
||||
|
||||
|
|
@ -17,6 +17,49 @@ class AbstractHierarchyController:
|
|||
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:
|
||||
"""Item representing folder entity on a server.
|
||||
|
||||
|
|
@ -40,6 +83,23 @@ class ProjectItem:
|
|||
}
|
||||
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):
|
||||
"""Converts folder item to data.
|
||||
|
||||
|
|
@ -79,7 +139,7 @@ def _get_project_items_from_entitiy(projects):
|
|||
"""
|
||||
|
||||
return [
|
||||
ProjectItem(project["name"], project["active"], project["library"])
|
||||
ProjectItem.from_entity(project)
|
||||
for project in projects
|
||||
]
|
||||
|
||||
|
|
@ -87,18 +147,29 @@ def _get_project_items_from_entitiy(projects):
|
|||
class ProjectsModel(object):
|
||||
def __init__(self, controller):
|
||||
self._projects_cache = CacheItem(default_factory=list)
|
||||
self._project_items_by_name = {}
|
||||
self._projects_by_name = {}
|
||||
self._project_statuses_cache = NestedCacheItem(
|
||||
levels=1, default_factory=list
|
||||
)
|
||||
self._projects_by_name = NestedCacheItem(
|
||||
levels=1, default_factory=list
|
||||
)
|
||||
|
||||
self._is_refreshing = False
|
||||
self._controller = controller
|
||||
|
||||
def reset(self):
|
||||
self._projects_cache.reset()
|
||||
self._project_items_by_name = {}
|
||||
self._projects_by_name = {}
|
||||
self._project_statuses_cache.reset()
|
||||
self._projects_by_name.reset()
|
||||
|
||||
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()
|
||||
|
||||
def get_project_items(self, sender):
|
||||
|
|
@ -117,12 +188,51 @@ class ProjectsModel(object):
|
|||
return self._projects_cache.get_data()
|
||||
|
||||
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
|
||||
if project_name:
|
||||
entity = ayon_api.get_project(project_name)
|
||||
self._projects_by_name[project_name] = entity
|
||||
return self._projects_by_name[project_name]
|
||||
project_cache.update_data(entity)
|
||||
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
|
||||
def _project_refresh_event_manager(self, sender):
|
||||
|
|
@ -143,6 +253,23 @@ class ProjectsModel(object):
|
|||
)
|
||||
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):
|
||||
if self._is_refreshing:
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -1,234 +1,15 @@
|
|||
import os
|
||||
import time
|
||||
import collections
|
||||
|
||||
import ayon_api
|
||||
import appdirs
|
||||
|
||||
from ayon_core.lib import NestedCacheItem
|
||||
|
||||
FileInfo = collections.namedtuple(
|
||||
"FileInfo",
|
||||
("path", "size", "modification_time")
|
||||
)
|
||||
|
||||
|
||||
class ThumbnailsCache:
|
||||
"""Cache of thumbnails on local storage.
|
||||
|
||||
Thumbnails are cached to appdirs to predefined directory. Each project has
|
||||
own subfolder with thumbnails -> that's because each project has own
|
||||
thumbnail id validation and file names are thumbnail ids with matching
|
||||
extension. Extensions are predefined (.png and .jpeg).
|
||||
|
||||
Cache has cleanup mechanism which is triggered on initialized by default.
|
||||
|
||||
The cleanup has 2 levels:
|
||||
1. soft cleanup which remove all files that are older then 'days_alive'
|
||||
2. max size cleanup which remove all files until the thumbnails folder
|
||||
contains less then 'max_filesize'
|
||||
- this is time consuming so it's not triggered automatically
|
||||
|
||||
Args:
|
||||
cleanup (bool): Trigger soft cleanup (Cleanup expired thumbnails).
|
||||
"""
|
||||
|
||||
# Lifetime of thumbnails (in seconds)
|
||||
# - default 3 days
|
||||
days_alive = 3
|
||||
# Max size of thumbnail directory (in bytes)
|
||||
# - default 2 Gb
|
||||
max_filesize = 2 * 1024 * 1024 * 1024
|
||||
|
||||
def __init__(self, cleanup=True):
|
||||
self._thumbnails_dir = None
|
||||
self._days_alive_secs = self.days_alive * 24 * 60 * 60
|
||||
if cleanup:
|
||||
self.cleanup()
|
||||
|
||||
def get_thumbnails_dir(self):
|
||||
"""Root directory where thumbnails are stored.
|
||||
|
||||
Returns:
|
||||
str: Path to thumbnails root.
|
||||
"""
|
||||
|
||||
if self._thumbnails_dir is None:
|
||||
# TODO use generic function
|
||||
directory = appdirs.user_data_dir("AYON", "Ynput")
|
||||
self._thumbnails_dir = os.path.join(directory, "thumbnails")
|
||||
return self._thumbnails_dir
|
||||
|
||||
thumbnails_dir = property(get_thumbnails_dir)
|
||||
|
||||
def get_thumbnails_dir_file_info(self):
|
||||
"""Get information about all files in thumbnails directory.
|
||||
|
||||
Returns:
|
||||
List[FileInfo]: List of file information about all files.
|
||||
"""
|
||||
|
||||
thumbnails_dir = self.thumbnails_dir
|
||||
files_info = []
|
||||
if not os.path.exists(thumbnails_dir):
|
||||
return files_info
|
||||
|
||||
for root, _, filenames in os.walk(thumbnails_dir):
|
||||
for filename in filenames:
|
||||
path = os.path.join(root, filename)
|
||||
files_info.append(FileInfo(
|
||||
path, os.path.getsize(path), os.path.getmtime(path)
|
||||
))
|
||||
return files_info
|
||||
|
||||
def get_thumbnails_dir_size(self, files_info=None):
|
||||
"""Got full size of thumbnail directory.
|
||||
|
||||
Args:
|
||||
files_info (List[FileInfo]): Prepared file information about
|
||||
files in thumbnail directory.
|
||||
|
||||
Returns:
|
||||
int: File size of all files in thumbnail directory.
|
||||
"""
|
||||
|
||||
if files_info is None:
|
||||
files_info = self.get_thumbnails_dir_file_info()
|
||||
|
||||
if not files_info:
|
||||
return 0
|
||||
|
||||
return sum(
|
||||
file_info.size
|
||||
for file_info in files_info
|
||||
)
|
||||
|
||||
def cleanup(self, check_max_size=False):
|
||||
"""Cleanup thumbnails directory.
|
||||
|
||||
Args:
|
||||
check_max_size (bool): Also cleanup files to match max size of
|
||||
thumbnails directory.
|
||||
"""
|
||||
|
||||
thumbnails_dir = self.get_thumbnails_dir()
|
||||
# Skip if thumbnails dir does not exist yet
|
||||
if not os.path.exists(thumbnails_dir):
|
||||
return
|
||||
|
||||
self._soft_cleanup(thumbnails_dir)
|
||||
if check_max_size:
|
||||
self._max_size_cleanup(thumbnails_dir)
|
||||
|
||||
def _soft_cleanup(self, thumbnails_dir):
|
||||
current_time = time.time()
|
||||
for root, _, filenames in os.walk(thumbnails_dir):
|
||||
for filename in filenames:
|
||||
path = os.path.join(root, filename)
|
||||
modification_time = os.path.getmtime(path)
|
||||
if current_time - modification_time > self._days_alive_secs:
|
||||
os.remove(path)
|
||||
|
||||
def _max_size_cleanup(self, thumbnails_dir):
|
||||
files_info = self.get_thumbnails_dir_file_info()
|
||||
size = self.get_thumbnails_dir_size(files_info)
|
||||
if size < self.max_filesize:
|
||||
return
|
||||
|
||||
sorted_file_info = collections.deque(
|
||||
sorted(files_info, key=lambda item: item.modification_time)
|
||||
)
|
||||
diff = size - self.max_filesize
|
||||
while diff > 0:
|
||||
if not sorted_file_info:
|
||||
break
|
||||
|
||||
file_info = sorted_file_info.popleft()
|
||||
diff -= file_info.size
|
||||
os.remove(file_info.path)
|
||||
|
||||
def get_thumbnail_filepath(self, project_name, thumbnail_id):
|
||||
"""Get thumbnail by thumbnail id.
|
||||
|
||||
Args:
|
||||
project_name (str): Name of project.
|
||||
thumbnail_id (str): Thumbnail id.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Path to thumbnail image or None if thumbnail
|
||||
is not cached yet.
|
||||
"""
|
||||
|
||||
if not thumbnail_id:
|
||||
return None
|
||||
|
||||
for ext in (
|
||||
".png",
|
||||
".jpeg",
|
||||
):
|
||||
filepath = os.path.join(
|
||||
self.thumbnails_dir, project_name, thumbnail_id + ext
|
||||
)
|
||||
if os.path.exists(filepath):
|
||||
return filepath
|
||||
return None
|
||||
|
||||
def get_project_dir(self, project_name):
|
||||
"""Path to root directory for specific project.
|
||||
|
||||
Args:
|
||||
project_name (str): Name of project for which root directory path
|
||||
should be returned.
|
||||
|
||||
Returns:
|
||||
str: Path to root of project's thumbnails.
|
||||
"""
|
||||
|
||||
return os.path.join(self.thumbnails_dir, project_name)
|
||||
|
||||
def make_sure_project_dir_exists(self, project_name):
|
||||
project_dir = self.get_project_dir(project_name)
|
||||
if not os.path.exists(project_dir):
|
||||
os.makedirs(project_dir)
|
||||
return project_dir
|
||||
|
||||
def store_thumbnail(self, project_name, thumbnail_id, content, mime_type):
|
||||
"""Store thumbnail to cache folder.
|
||||
|
||||
Args:
|
||||
project_name (str): Project where the thumbnail belong to.
|
||||
thumbnail_id (str): Id of thumbnail.
|
||||
content (bytes): Byte content of thumbnail file.
|
||||
mime_data (str): Type of content.
|
||||
|
||||
Returns:
|
||||
str: Path to cached thumbnail image file.
|
||||
"""
|
||||
|
||||
if mime_type == "image/png":
|
||||
ext = ".png"
|
||||
elif mime_type == "image/jpeg":
|
||||
ext = ".jpeg"
|
||||
else:
|
||||
raise ValueError(
|
||||
"Unknown mime type for thumbnail \"{}\"".format(mime_type))
|
||||
|
||||
project_dir = self.make_sure_project_dir_exists(project_name)
|
||||
thumbnail_path = os.path.join(project_dir, thumbnail_id + ext)
|
||||
with open(thumbnail_path, "wb") as stream:
|
||||
stream.write(content)
|
||||
|
||||
current_time = time.time()
|
||||
os.utime(thumbnail_path, (current_time, current_time))
|
||||
|
||||
return thumbnail_path
|
||||
from ayon_core.pipeline.thumbnails import get_thumbnail_path
|
||||
|
||||
|
||||
class ThumbnailsModel:
|
||||
entity_cache_lifetime = 240 # In seconds
|
||||
|
||||
def __init__(self):
|
||||
self._thumbnail_cache = ThumbnailsCache()
|
||||
self._paths_cache = collections.defaultdict(dict)
|
||||
self._folders_cache = NestedCacheItem(
|
||||
levels=2, lifetime=self.entity_cache_lifetime)
|
||||
|
|
@ -283,28 +64,7 @@ class ThumbnailsModel:
|
|||
if thumbnail_id in project_cache:
|
||||
return project_cache[thumbnail_id]
|
||||
|
||||
filepath = self._thumbnail_cache.get_thumbnail_filepath(
|
||||
project_name, thumbnail_id
|
||||
)
|
||||
if filepath is not None:
|
||||
project_cache[thumbnail_id] = filepath
|
||||
return filepath
|
||||
|
||||
# 'ayon_api' had a bug, public function
|
||||
# 'get_thumbnail_by_id' did not return output of
|
||||
# 'ServerAPI' method.
|
||||
con = ayon_api.get_server_api_connection()
|
||||
result = con.get_thumbnail_by_id(project_name, thumbnail_id)
|
||||
if result is None:
|
||||
pass
|
||||
|
||||
elif result.is_valid:
|
||||
filepath = self._thumbnail_cache.store_thumbnail(
|
||||
project_name,
|
||||
thumbnail_id,
|
||||
result.content,
|
||||
result.content_type
|
||||
)
|
||||
filepath = get_thumbnail_path(project_name, thumbnail_id)
|
||||
project_cache[thumbnail_id] = filepath
|
||||
return filepath
|
||||
|
||||
|
|
|
|||
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)
|
||||
|
||||
|
||||
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):
|
||||
def __init__(self, controller, parent):
|
||||
super(ActionsWidget, self).__init__(parent)
|
||||
|
|
@ -316,10 +344,7 @@ class ActionsWidget(QtWidgets.QWidget):
|
|||
|
||||
model = ActionsQtModel(controller)
|
||||
|
||||
proxy_model = QtCore.QSortFilterProxyModel()
|
||||
proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)
|
||||
proxy_model.setSortRole(ACTION_SORT_ROLE)
|
||||
|
||||
proxy_model = ActionsProxyModel()
|
||||
proxy_model.setSourceModel(model)
|
||||
view.setModel(proxy_model)
|
||||
|
||||
|
|
@ -359,7 +384,8 @@ class ActionsWidget(QtWidgets.QWidget):
|
|||
def _on_model_refresh(self):
|
||||
self._proxy_model.sort(0)
|
||||
# Force repaint all items
|
||||
self._view.update()
|
||||
viewport = self._view.viewport()
|
||||
viewport.update()
|
||||
|
||||
def _on_animation(self):
|
||||
time_now = time.time()
|
||||
|
|
|
|||
|
|
@ -114,6 +114,7 @@ class VersionItem:
|
|||
thumbnail_id (Union[str, None]): Thumbnail id.
|
||||
published_time (Union[str, None]): Published time in format
|
||||
'%Y%m%dT%H%M%SZ'.
|
||||
status (Union[str, None]): Status name.
|
||||
author (Union[str, None]): Author.
|
||||
frame_range (Union[str, None]): Frame range.
|
||||
duration (Union[int, None]): Duration.
|
||||
|
|
@ -132,6 +133,7 @@ class VersionItem:
|
|||
thumbnail_id,
|
||||
published_time,
|
||||
author,
|
||||
status,
|
||||
frame_range,
|
||||
duration,
|
||||
handles,
|
||||
|
|
@ -146,6 +148,7 @@ class VersionItem:
|
|||
self.is_hero = is_hero
|
||||
self.published_time = published_time
|
||||
self.author = author
|
||||
self.status = status
|
||||
self.frame_range = frame_range
|
||||
self.duration = duration
|
||||
self.handles = handles
|
||||
|
|
@ -185,6 +188,7 @@ class VersionItem:
|
|||
"is_hero": self.is_hero,
|
||||
"published_time": self.published_time,
|
||||
"author": self.author,
|
||||
"status": self.status,
|
||||
"frame_range": self.frame_range,
|
||||
"duration": self.duration,
|
||||
"handles": self.handles,
|
||||
|
|
@ -488,6 +492,27 @@ class FrontendLoaderController(_BaseLoaderController):
|
|||
|
||||
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
|
||||
def get_product_items(self, project_name, folder_ids, sender=None):
|
||||
"""Product items for folder ids.
|
||||
|
|
|
|||
|
|
@ -180,6 +180,11 @@ class LoaderController(BackendLoaderController, FrontendLoaderController):
|
|||
def get_project_items(self, sender=None):
|
||||
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):
|
||||
return self._hierarchy_model.get_folder_items(project_name, sender)
|
||||
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ def version_item_from_entity(version):
|
|||
thumbnail_id=version["thumbnailId"],
|
||||
published_time=published_time,
|
||||
author=author,
|
||||
status=version["status"],
|
||||
frame_range=frame_range,
|
||||
duration=duration,
|
||||
handles=handles,
|
||||
|
|
@ -526,8 +527,11 @@ class ProductsModel:
|
|||
products = list(ayon_api.get_products(project_name, **kwargs))
|
||||
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(
|
||||
project_name, product_ids=product_ids
|
||||
project_name, product_ids=product_ids, fields=fields
|
||||
)
|
||||
|
||||
return self._create_product_items(
|
||||
|
|
|
|||
|
|
@ -104,7 +104,10 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate):
|
|||
style = QtWidgets.QApplication.style()
|
||||
|
||||
style.drawControl(
|
||||
style.CE_ItemViewItem, option, painter, option.widget
|
||||
QtWidgets.QCommonStyle.CE_ItemViewItem,
|
||||
option,
|
||||
painter,
|
||||
option.widget
|
||||
)
|
||||
|
||||
painter.save()
|
||||
|
|
@ -116,9 +119,14 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate):
|
|||
pen.setColor(fg_color)
|
||||
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(
|
||||
style.PM_FocusFrameHMargin, option, option.widget
|
||||
QtWidgets.QCommonStyle.PM_FocusFrameHMargin,
|
||||
option,
|
||||
option.widget
|
||||
) + 1
|
||||
|
||||
painter.drawText(
|
||||
|
|
|
|||
|
|
@ -22,18 +22,22 @@ VERSION_HERO_ROLE = QtCore.Qt.UserRole + 11
|
|||
VERSION_NAME_ROLE = QtCore.Qt.UserRole + 12
|
||||
VERSION_NAME_EDIT_ROLE = QtCore.Qt.UserRole + 13
|
||||
VERSION_PUBLISH_TIME_ROLE = QtCore.Qt.UserRole + 14
|
||||
VERSION_AUTHOR_ROLE = QtCore.Qt.UserRole + 15
|
||||
VERSION_FRAME_RANGE_ROLE = QtCore.Qt.UserRole + 16
|
||||
VERSION_DURATION_ROLE = QtCore.Qt.UserRole + 17
|
||||
VERSION_HANDLES_ROLE = QtCore.Qt.UserRole + 18
|
||||
VERSION_STEP_ROLE = QtCore.Qt.UserRole + 19
|
||||
VERSION_AVAILABLE_ROLE = QtCore.Qt.UserRole + 20
|
||||
VERSION_THUMBNAIL_ID_ROLE = QtCore.Qt.UserRole + 21
|
||||
ACTIVE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 22
|
||||
REMOTE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 23
|
||||
REPRESENTATIONS_COUNT_ROLE = QtCore.Qt.UserRole + 24
|
||||
SYNC_ACTIVE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 25
|
||||
SYNC_REMOTE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 26
|
||||
VERSION_STATUS_NAME_ROLE = QtCore.Qt.UserRole + 15
|
||||
VERSION_STATUS_SHORT_ROLE = QtCore.Qt.UserRole + 16
|
||||
VERSION_STATUS_COLOR_ROLE = QtCore.Qt.UserRole + 17
|
||||
VERSION_STATUS_ICON_ROLE = QtCore.Qt.UserRole + 18
|
||||
VERSION_AUTHOR_ROLE = QtCore.Qt.UserRole + 19
|
||||
VERSION_FRAME_RANGE_ROLE = QtCore.Qt.UserRole + 20
|
||||
VERSION_DURATION_ROLE = QtCore.Qt.UserRole + 21
|
||||
VERSION_HANDLES_ROLE = QtCore.Qt.UserRole + 22
|
||||
VERSION_STEP_ROLE = QtCore.Qt.UserRole + 23
|
||||
VERSION_AVAILABLE_ROLE = QtCore.Qt.UserRole + 24
|
||||
VERSION_THUMBNAIL_ID_ROLE = QtCore.Qt.UserRole + 25
|
||||
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):
|
||||
|
|
@ -44,6 +48,7 @@ class ProductsModel(QtGui.QStandardItemModel):
|
|||
"Product type",
|
||||
"Folder",
|
||||
"Version",
|
||||
"Status",
|
||||
"Time",
|
||||
"Author",
|
||||
"Frames",
|
||||
|
|
@ -69,11 +74,35 @@ class ProductsModel(QtGui.QStandardItemModel):
|
|||
]
|
||||
]
|
||||
|
||||
version_col = column_labels.index("Version")
|
||||
published_time_col = column_labels.index("Time")
|
||||
product_name_col = column_labels.index("Product name")
|
||||
product_type_col = column_labels.index("Product type")
|
||||
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")
|
||||
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):
|
||||
super(ProductsModel, self).__init__()
|
||||
|
|
@ -96,6 +125,7 @@ class ProductsModel(QtGui.QStandardItemModel):
|
|||
|
||||
self._last_project_name = None
|
||||
self._last_folder_ids = []
|
||||
self._last_project_statuses = {}
|
||||
|
||||
def get_product_item_indexes(self):
|
||||
return [
|
||||
|
|
@ -141,6 +171,15 @@ class ProductsModel(QtGui.QStandardItemModel):
|
|||
if not index.isValid():
|
||||
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()
|
||||
if col == 0:
|
||||
return super(ProductsModel, self).data(index, role)
|
||||
|
|
@ -168,29 +207,8 @@ class ProductsModel(QtGui.QStandardItemModel):
|
|||
if role == QtCore.Qt.DisplayRole:
|
||||
if not index.data(PRODUCT_ID_ROLE):
|
||||
return None
|
||||
if col == self.version_col:
|
||||
role = VERSION_NAME_ROLE
|
||||
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:
|
||||
role = self._display_role_mapping.get(col)
|
||||
if role is None:
|
||||
return None
|
||||
|
||||
index = self.index(index.row(), 0, index.parent())
|
||||
|
|
@ -312,6 +330,7 @@ class ProductsModel(QtGui.QStandardItemModel):
|
|||
version_item.published_time, VERSION_PUBLISH_TIME_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.duration, VERSION_DURATION_ROLE)
|
||||
model_item.setData(version_item.handles, VERSION_HANDLES_ROLE)
|
||||
|
|
@ -393,6 +412,11 @@ class ProductsModel(QtGui.QStandardItemModel):
|
|||
|
||||
self._last_project_name = project_name
|
||||
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(
|
||||
project_name
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from ayon_core.tools.utils import (
|
|||
RecursiveSortFilterProxyModel,
|
||||
DeselectableTreeView,
|
||||
)
|
||||
from ayon_core.tools.utils.delegates import PrettyTimeDelegate
|
||||
from ayon_core.tools.utils.delegates import PrettyTimeDelegate, StatusDelegate
|
||||
|
||||
from .products_model import (
|
||||
ProductsModel,
|
||||
|
|
@ -17,12 +17,16 @@ from .products_model import (
|
|||
FOLDER_ID_ROLE,
|
||||
PRODUCT_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,
|
||||
)
|
||||
from .products_delegates import (
|
||||
VersionDelegate,
|
||||
LoadedInSceneDelegate,
|
||||
SiteSyncDelegate
|
||||
SiteSyncDelegate,
|
||||
)
|
||||
from .actions_utils import show_actions_menu
|
||||
|
||||
|
|
@ -89,6 +93,7 @@ class ProductsWidget(QtWidgets.QWidget):
|
|||
90, # Product type
|
||||
130, # Folder label
|
||||
60, # Version
|
||||
100, # Status
|
||||
125, # Time
|
||||
75, # Author
|
||||
75, # Frames
|
||||
|
|
@ -128,20 +133,24 @@ class ProductsWidget(QtWidgets.QWidget):
|
|||
products_view.setColumnWidth(idx, width)
|
||||
|
||||
version_delegate = VersionDelegate()
|
||||
products_view.setItemDelegateForColumn(
|
||||
products_model.version_col, version_delegate)
|
||||
|
||||
time_delegate = PrettyTimeDelegate()
|
||||
products_view.setItemDelegateForColumn(
|
||||
products_model.published_time_col, time_delegate)
|
||||
|
||||
status_delegate = StatusDelegate(
|
||||
VERSION_STATUS_NAME_ROLE,
|
||||
VERSION_STATUS_SHORT_ROLE,
|
||||
VERSION_STATUS_COLOR_ROLE,
|
||||
VERSION_STATUS_ICON_ROLE,
|
||||
)
|
||||
in_scene_delegate = LoadedInSceneDelegate()
|
||||
products_view.setItemDelegateForColumn(
|
||||
products_model.in_scene_col, in_scene_delegate)
|
||||
|
||||
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.setContentsMargins(0, 0, 0, 0)
|
||||
|
|
@ -175,6 +184,7 @@ class ProductsWidget(QtWidgets.QWidget):
|
|||
|
||||
self._version_delegate = version_delegate
|
||||
self._time_delegate = time_delegate
|
||||
self._status_delegate = status_delegate
|
||||
self._in_scene_delegate = in_scene_delegate
|
||||
self._sitesync_delegate = sitesync_delegate
|
||||
|
||||
|
|
|
|||
|
|
@ -335,9 +335,7 @@ class LoaderWindow(QtWidgets.QWidget):
|
|||
|
||||
def closeEvent(self, 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
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ class SelectionTypes:
|
|||
class BaseGroupWidget(QtWidgets.QWidget):
|
||||
selected = QtCore.Signal(str, str, str)
|
||||
removed_selected = QtCore.Signal()
|
||||
double_clicked = QtCore.Signal()
|
||||
|
||||
def __init__(self, group_name, parent):
|
||||
super(BaseGroupWidget, self).__init__(parent)
|
||||
|
|
@ -192,6 +193,7 @@ class ConvertorItemsGroupWidget(BaseGroupWidget):
|
|||
else:
|
||||
widget = ConvertorItemCardWidget(item, self)
|
||||
widget.selected.connect(self._on_widget_selection)
|
||||
widget.double_clicked(self.double_clicked)
|
||||
self._widgets_by_id[item.id] = widget
|
||||
self._content_layout.insertWidget(widget_idx, widget)
|
||||
widget_idx += 1
|
||||
|
|
@ -254,6 +256,7 @@ class InstanceGroupWidget(BaseGroupWidget):
|
|||
)
|
||||
widget.selected.connect(self._on_widget_selection)
|
||||
widget.active_changed.connect(self._on_active_changed)
|
||||
widget.double_clicked.connect(self.double_clicked)
|
||||
self._widgets_by_id[instance.id] = widget
|
||||
self._content_layout.insertWidget(widget_idx, widget)
|
||||
widget_idx += 1
|
||||
|
|
@ -271,6 +274,7 @@ class CardWidget(BaseClickableFrame):
|
|||
# Group identifier of card
|
||||
# - this must be set because if send when mouse is released with card id
|
||||
_group_identifier = None
|
||||
double_clicked = QtCore.Signal()
|
||||
|
||||
def __init__(self, parent):
|
||||
super(CardWidget, self).__init__(parent)
|
||||
|
|
@ -279,6 +283,11 @@ class CardWidget(BaseClickableFrame):
|
|||
self._selected = False
|
||||
self._id = None
|
||||
|
||||
def mouseDoubleClickEvent(self, event):
|
||||
super(CardWidget, self).mouseDoubleClickEvent(event)
|
||||
if self._is_valid_double_click(event):
|
||||
self.double_clicked.emit()
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
"""Id of card."""
|
||||
|
|
@ -312,6 +321,9 @@ class CardWidget(BaseClickableFrame):
|
|||
|
||||
self.selected.emit(self._id, self._group_identifier, selection_type)
|
||||
|
||||
def _is_valid_double_click(self, event):
|
||||
return True
|
||||
|
||||
|
||||
class ContextCardWidget(CardWidget):
|
||||
"""Card for global context.
|
||||
|
|
@ -527,6 +539,15 @@ class InstanceCardWidget(CardWidget):
|
|||
def _on_expend_clicked(self):
|
||||
self._set_expanded()
|
||||
|
||||
def _is_valid_double_click(self, event):
|
||||
widget = self.childAt(event.pos())
|
||||
if (
|
||||
widget is self._active_checkbox
|
||||
or widget is self._expand_btn
|
||||
):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class InstanceCardView(AbstractInstanceView):
|
||||
"""Publish access to card view.
|
||||
|
|
@ -534,6 +555,8 @@ class InstanceCardView(AbstractInstanceView):
|
|||
Wrapper of all widgets in card view.
|
||||
"""
|
||||
|
||||
double_clicked = QtCore.Signal()
|
||||
|
||||
def __init__(self, controller, parent):
|
||||
super(InstanceCardView, self).__init__(parent)
|
||||
|
||||
|
|
@ -715,6 +738,7 @@ class InstanceCardView(AbstractInstanceView):
|
|||
)
|
||||
group_widget.active_changed.connect(self._on_active_changed)
|
||||
group_widget.selected.connect(self._on_widget_selection)
|
||||
group_widget.double_clicked.connect(self.double_clicked)
|
||||
self._content_layout.insertWidget(widget_idx, group_widget)
|
||||
self._widgets_by_group[group_name] = group_widget
|
||||
|
||||
|
|
@ -755,6 +779,7 @@ class InstanceCardView(AbstractInstanceView):
|
|||
|
||||
widget = ContextCardWidget(self._content_widget)
|
||||
widget.selected.connect(self._on_widget_selection)
|
||||
widget.double_clicked.connect(self.double_clicked)
|
||||
|
||||
self._context_widget = widget
|
||||
|
||||
|
|
@ -778,6 +803,7 @@ class InstanceCardView(AbstractInstanceView):
|
|||
CONVERTOR_ITEM_GROUP, self._content_widget
|
||||
)
|
||||
group_widget.selected.connect(self._on_widget_selection)
|
||||
group_widget.double_clicked.connect(self.double_clicked)
|
||||
self._content_layout.insertWidget(1, group_widget)
|
||||
self._convertor_items_group = group_widget
|
||||
|
||||
|
|
|
|||
|
|
@ -110,6 +110,7 @@ class InstanceListItemWidget(QtWidgets.QWidget):
|
|||
This is required to be able use custom checkbox on custom place.
|
||||
"""
|
||||
active_changed = QtCore.Signal(str, bool)
|
||||
double_clicked = QtCore.Signal()
|
||||
|
||||
def __init__(self, instance, parent):
|
||||
super(InstanceListItemWidget, self).__init__(parent)
|
||||
|
|
@ -149,6 +150,12 @@ class InstanceListItemWidget(QtWidgets.QWidget):
|
|||
|
||||
self._set_valid_property(instance.has_valid_context)
|
||||
|
||||
def mouseDoubleClickEvent(self, event):
|
||||
widget = self.childAt(event.pos())
|
||||
super(InstanceListItemWidget, self).mouseDoubleClickEvent(event)
|
||||
if widget is not self._active_checkbox:
|
||||
self.double_clicked.emit()
|
||||
|
||||
def _set_valid_property(self, valid):
|
||||
if self._has_valid_context == valid:
|
||||
return
|
||||
|
|
@ -209,6 +216,8 @@ class InstanceListItemWidget(QtWidgets.QWidget):
|
|||
|
||||
class ListContextWidget(QtWidgets.QFrame):
|
||||
"""Context (or global attributes) widget."""
|
||||
double_clicked = QtCore.Signal()
|
||||
|
||||
def __init__(self, parent):
|
||||
super(ListContextWidget, self).__init__(parent)
|
||||
|
||||
|
|
@ -225,6 +234,10 @@ class ListContextWidget(QtWidgets.QFrame):
|
|||
|
||||
self.label_widget = label_widget
|
||||
|
||||
def mouseDoubleClickEvent(self, event):
|
||||
super(ListContextWidget, self).mouseDoubleClickEvent(event)
|
||||
self.double_clicked.emit()
|
||||
|
||||
|
||||
class InstanceListGroupWidget(QtWidgets.QFrame):
|
||||
"""Widget representing group of instances.
|
||||
|
|
@ -317,6 +330,7 @@ class InstanceListGroupWidget(QtWidgets.QFrame):
|
|||
class InstanceTreeView(QtWidgets.QTreeView):
|
||||
"""View showing instances and their groups."""
|
||||
toggle_requested = QtCore.Signal(int)
|
||||
double_clicked = QtCore.Signal()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(InstanceTreeView, self).__init__(*args, **kwargs)
|
||||
|
|
@ -425,6 +439,9 @@ class InstanceListView(AbstractInstanceView):
|
|||
|
||||
This is public access to and from list view.
|
||||
"""
|
||||
|
||||
double_clicked = QtCore.Signal()
|
||||
|
||||
def __init__(self, controller, parent):
|
||||
super(InstanceListView, self).__init__(parent)
|
||||
|
||||
|
|
@ -454,6 +471,7 @@ class InstanceListView(AbstractInstanceView):
|
|||
instance_view.collapsed.connect(self._on_collapse)
|
||||
instance_view.expanded.connect(self._on_expand)
|
||||
instance_view.toggle_requested.connect(self._on_toggle_request)
|
||||
instance_view.double_clicked.connect(self.double_clicked)
|
||||
|
||||
self._group_items = {}
|
||||
self._group_widgets = {}
|
||||
|
|
@ -687,6 +705,7 @@ class InstanceListView(AbstractInstanceView):
|
|||
self._active_toggle_enabled
|
||||
)
|
||||
widget.active_changed.connect(self._on_active_changed)
|
||||
widget.double_clicked.connect(self.double_clicked)
|
||||
self._instance_view.setIndexWidget(proxy_index, widget)
|
||||
self._widgets_by_id[instance.id] = widget
|
||||
|
||||
|
|
@ -717,6 +736,7 @@ class InstanceListView(AbstractInstanceView):
|
|||
)
|
||||
proxy_index = self._proxy_model.mapFromSource(index)
|
||||
widget = ListContextWidget(self._instance_view)
|
||||
widget.double_clicked.connect(self.double_clicked)
|
||||
self._instance_view.setIndexWidget(proxy_index, widget)
|
||||
|
||||
self._context_widget = widget
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ class OverviewWidget(QtWidgets.QFrame):
|
|||
instance_context_changed = QtCore.Signal()
|
||||
create_requested = QtCore.Signal()
|
||||
convert_requested = QtCore.Signal()
|
||||
publish_tab_requested = QtCore.Signal()
|
||||
|
||||
anim_end_value = 200
|
||||
anim_duration = 200
|
||||
|
|
@ -113,9 +114,15 @@ class OverviewWidget(QtWidgets.QFrame):
|
|||
product_list_view.selection_changed.connect(
|
||||
self._on_product_change
|
||||
)
|
||||
product_list_view.double_clicked.connect(
|
||||
self.publish_tab_requested
|
||||
)
|
||||
product_view_cards.selection_changed.connect(
|
||||
self._on_product_change
|
||||
)
|
||||
product_view_cards.double_clicked.connect(
|
||||
self.publish_tab_requested
|
||||
)
|
||||
# Active instances changed
|
||||
product_list_view.active_changed.connect(
|
||||
self._on_active_changed
|
||||
|
|
|
|||
|
|
@ -258,6 +258,9 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
overview_widget.convert_requested.connect(
|
||||
self._on_convert_requested
|
||||
)
|
||||
overview_widget.publish_tab_requested.connect(
|
||||
self._go_to_publish_tab
|
||||
)
|
||||
|
||||
save_btn.clicked.connect(self._on_save_clicked)
|
||||
reset_btn.clicked.connect(self._on_reset_clicked)
|
||||
|
|
|
|||
|
|
@ -723,7 +723,6 @@ class ProjectPushItemProcess:
|
|||
dst_project_name = self._item.dst_project_name
|
||||
dst_folder_id = self._item.dst_folder_id
|
||||
dst_task_name = self._item.dst_task_name
|
||||
dst_task_name_low = dst_task_name.lower()
|
||||
new_folder_name = self._item.new_folder_name
|
||||
if not dst_folder_id and not new_folder_name:
|
||||
self._status.set_failed(
|
||||
|
|
@ -765,7 +764,7 @@ class ProjectPushItemProcess:
|
|||
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:
|
||||
self._status.set_failed(
|
||||
f"Could find task with name \"{dst_task_name}\""
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import ayon_api
|
||||
|
||||
from ayon_core.lib.events import QueuedEventSystem
|
||||
from ayon_core.host import ILoadHost
|
||||
from ayon_core.host import HostBase
|
||||
from ayon_core.pipeline import (
|
||||
registered_host,
|
||||
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:
|
||||
|
|
@ -28,11 +28,16 @@ class SceneInventoryController:
|
|||
self._current_folder_id = None
|
||||
self._current_folder_set = False
|
||||
|
||||
self._containers_model = ContainersModel(self)
|
||||
self._sitesync_model = SiteSyncModel(self)
|
||||
# Switch dialog requirements
|
||||
self._hierarchy_model = HierarchyModel(self)
|
||||
self._projects_model = ProjectsModel(self)
|
||||
self._event_system = self._create_event_system()
|
||||
|
||||
def get_host(self) -> HostBase:
|
||||
return self._host
|
||||
|
||||
def emit_event(self, topic, data=None, source=None):
|
||||
if data is None:
|
||||
data = {}
|
||||
|
|
@ -47,6 +52,7 @@ class SceneInventoryController:
|
|||
self._current_folder_id = None
|
||||
self._current_folder_set = False
|
||||
|
||||
self._containers_model.reset()
|
||||
self._sitesync_model.reset()
|
||||
self._hierarchy_model.reset()
|
||||
|
||||
|
|
@ -80,13 +86,32 @@ class SceneInventoryController:
|
|||
self._current_folder_set = True
|
||||
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):
|
||||
host = self._host
|
||||
if isinstance(host, ILoadHost):
|
||||
return list(host.get_containers())
|
||||
elif hasattr(host, "ls"):
|
||||
return list(host.ls())
|
||||
return []
|
||||
return self._containers_model.get_containers()
|
||||
|
||||
def get_containers_by_item_ids(self, item_ids):
|
||||
return self._containers_model.get_containers_by_item_ids(item_ids)
|
||||
|
||||
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
|
||||
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 .model import VERSION_LABEL_ROLE
|
||||
|
||||
|
||||
class VersionDelegate(QtWidgets.QStyledItemDelegate):
|
||||
"""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):
|
||||
fg_color = index.data(QtCore.Qt.ForegroundRole)
|
||||
if fg_color:
|
||||
|
|
@ -44,7 +16,7 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate):
|
|||
fg_color = None
|
||||
|
||||
if not fg_color:
|
||||
return super(VersionDelegate, self).paint(painter, option, index)
|
||||
return super().paint(painter, option, index)
|
||||
|
||||
if option.widget:
|
||||
style = option.widget.style()
|
||||
|
|
@ -60,9 +32,7 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate):
|
|||
|
||||
painter.save()
|
||||
|
||||
text = self.displayText(
|
||||
index.data(QtCore.Qt.DisplayRole), option.locale
|
||||
)
|
||||
text = index.data(VERSION_LABEL_ROLE)
|
||||
pen = painter.pen()
|
||||
pen.setColor(fg_color)
|
||||
painter.setPen(pen)
|
||||
|
|
@ -82,77 +52,3 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate):
|
|||
)
|
||||
|
||||
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 logging
|
||||
import uuid
|
||||
|
||||
from collections import defaultdict
|
||||
import collections
|
||||
|
||||
import ayon_api
|
||||
from qtpy import QtCore, QtGui
|
||||
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.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):
|
||||
"""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):
|
||||
class InventoryModel(QtGui.QStandardItemModel):
|
||||
"""The model for the inventory"""
|
||||
|
||||
Columns = [
|
||||
column_labels = [
|
||||
"Name",
|
||||
"version",
|
||||
"count",
|
||||
"productType",
|
||||
"group",
|
||||
"loader",
|
||||
"objectName",
|
||||
"active_site",
|
||||
"remote_site",
|
||||
"Version",
|
||||
"Status",
|
||||
"Count",
|
||||
"Product type",
|
||||
"Group",
|
||||
"Loader",
|
||||
"Object name",
|
||||
"Active site",
|
||||
"Remote site",
|
||||
]
|
||||
active_site_col = Columns.index("active_site")
|
||||
remote_site_col = Columns.index("remote_site")
|
||||
name_col = column_labels.index("Name")
|
||||
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)
|
||||
CHILD_OUTDATED_COLOR = QtGui.QColor(200, 160, 30)
|
||||
GRAYOUT_COLOR = QtGui.QColor(160, 160, 160)
|
||||
|
||||
UniqueRole = QtCore.Qt.UserRole + 2 # unique label role
|
||||
|
||||
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._controller = controller
|
||||
|
|
@ -60,103 +116,217 @@ class InventoryModel(TreeModel):
|
|||
|
||||
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):
|
||||
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):
|
||||
if not index.isValid():
|
||||
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:
|
||||
# Make top-level entries bold
|
||||
if item.get("isGroupNode") or item.get("isNotSet"): # group-item
|
||||
font = QtGui.QFont()
|
||||
font.setBold(True)
|
||||
return font
|
||||
elif role == QtCore.Qt.DecorationRole:
|
||||
role = self.decoration_role_by_column.get(col)
|
||||
if role is None:
|
||||
return None
|
||||
|
||||
if role == QtCore.Qt.ForegroundRole:
|
||||
# Set the text color to the OUTDATED_COLOR when the
|
||||
# collected version is not the same as the highest version
|
||||
key = self.Columns[index.column()]
|
||||
if key == "version": # version
|
||||
if item.get("isGroupNode"): # group-item
|
||||
if self.outdated(item):
|
||||
return self.OUTDATED_COLOR
|
||||
elif role == QtCore.Qt.ForegroundRole:
|
||||
role = self.foreground_role_by_column.get(col)
|
||||
if role is None:
|
||||
return None
|
||||
|
||||
if self._hierarchy_view:
|
||||
# If current group is not outdated, check if any
|
||||
# outdated children.
|
||||
for _node in walk_hierarchy(item):
|
||||
if self.outdated(_node):
|
||||
return self.CHILD_OUTDATED_COLOR
|
||||
else:
|
||||
if col != 0:
|
||||
index = self.index(index.row(), 0, index.parent())
|
||||
|
||||
if self._hierarchy_view:
|
||||
# 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)
|
||||
return super().data(index, role)
|
||||
|
||||
def set_hierarchy_view(self, state):
|
||||
"""Set whether to display products in hierarchy view."""
|
||||
|
|
@ -165,299 +335,34 @@ class InventoryModel(TreeModel):
|
|||
if state != self._hierarchy_view:
|
||||
self._hierarchy_view = state
|
||||
|
||||
def refresh(self, selected=None, containers=None):
|
||||
"""Refresh the model"""
|
||||
|
||||
# for debugging or testing, injecting items from outside
|
||||
if containers is None:
|
||||
containers = self._controller.get_containers()
|
||||
|
||||
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)
|
||||
def get_outdated_item_ids(self, ignore_hero=True):
|
||||
outdated_item_ids = []
|
||||
root_item = self.invisibleRootItem()
|
||||
for row in range(root_item.rowCount()):
|
||||
group_item = root_item.child(row)
|
||||
if group_item.data(VERSION_IS_LATEST_ROLE):
|
||||
continue
|
||||
|
||||
version_entity = versions_by_id.get(representation["versionId"])
|
||||
if not version_entity:
|
||||
not_found["version"].extend(group_containers)
|
||||
not_found_ids.append(repre_id)
|
||||
if ignore_hero and group_item.data(VERSION_IS_HERO_ROLE):
|
||||
continue
|
||||
|
||||
product_entity = products_by_id.get(version_entity["productId"])
|
||||
if not product_entity:
|
||||
not_found["product"].extend(group_containers)
|
||||
not_found_ids.append(repre_id)
|
||||
continue
|
||||
for idx in range(group_item.rowCount()):
|
||||
item = group_item.child(idx)
|
||||
outdated_item_ids.append(item.data(ITEM_ID_ROLE))
|
||||
return outdated_item_ids
|
||||
|
||||
folder_entity = folders_by_id.get(product_entity["folderId"])
|
||||
if not folder_entity:
|
||||
not_found["folder"].extend(group_containers)
|
||||
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
|
||||
def _clear_items(self):
|
||||
root_item = self.invisibleRootItem()
|
||||
root_item.removeRows(0, root_item.rowCount())
|
||||
|
||||
|
||||
class FilterProxyModel(QtCore.QSortFilterProxyModel):
|
||||
"""Filter model to where key column's value is in the filtered tags"""
|
||||
|
||||
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._hierarchy_view = False
|
||||
|
||||
|
|
@ -467,28 +372,23 @@ class FilterProxyModel(QtCore.QSortFilterProxyModel):
|
|||
|
||||
# Always allow bottom entries (individual containers), since their
|
||||
# parent group hidden if it wouldn't have been validated.
|
||||
rows = model.rowCount(source_index)
|
||||
if not rows:
|
||||
if source_index.data(IS_CONTAINER_ITEM_ROLE):
|
||||
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:
|
||||
# When filtering to outdated we filter the up to date entries
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
def set_filter_outdated(self, state):
|
||||
|
|
@ -505,37 +405,6 @@ class FilterProxyModel(QtCore.QSortFilterProxyModel):
|
|||
if state != self._hierarchy_view:
|
||||
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):
|
||||
"""Return whether row matches regex pattern.
|
||||
|
||||
|
|
@ -548,38 +417,31 @@ class FilterProxyModel(QtCore.QSortFilterProxyModel):
|
|||
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()
|
||||
column = self.filterKeyColumn()
|
||||
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)
|
||||
key = model.data(index, role)
|
||||
if re.search(pattern, key, re.IGNORECASE):
|
||||
value = model.data(index, role)
|
||||
if regex.search(value):
|
||||
return True
|
||||
|
||||
if matches(row, parent, pattern):
|
||||
return True
|
||||
for idx in range(model.rowCount(index)):
|
||||
matches_queue.append((idx, index))
|
||||
|
||||
# Also allow if any of the children matches
|
||||
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
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
from .containers import ContainersModel
|
||||
from .sitesync import SiteSyncModel
|
||||
|
||||
|
||||
__all__ = (
|
||||
"ContainersModel",
|
||||
"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
|
||||
|
||||
from ayon_core import style, resources
|
||||
from ayon_core.tools.utils.lib import (
|
||||
preserve_expanded_rows,
|
||||
preserve_selection,
|
||||
)
|
||||
from ayon_core.tools.utils import PlaceholderLineEdit
|
||||
|
||||
from ayon_core.tools.sceneinventory import SceneInventoryController
|
||||
|
||||
from .delegates import VersionDelegate
|
||||
from .model import (
|
||||
InventoryModel,
|
||||
FilterProxyModel
|
||||
)
|
||||
from .view import SceneInventoryView
|
||||
|
||||
|
||||
|
|
@ -20,7 +13,7 @@ class SceneInventoryWindow(QtWidgets.QDialog):
|
|||
"""Scene Inventory window"""
|
||||
|
||||
def __init__(self, controller=None, parent=None):
|
||||
super(SceneInventoryWindow, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
|
||||
if controller is None:
|
||||
controller = SceneInventoryController()
|
||||
|
|
@ -33,10 +26,9 @@ class SceneInventoryWindow(QtWidgets.QDialog):
|
|||
|
||||
self.resize(1100, 480)
|
||||
|
||||
# region control
|
||||
|
||||
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(
|
||||
"Filter to outdated", self
|
||||
|
|
@ -44,52 +36,30 @@ class SceneInventoryWindow(QtWidgets.QDialog):
|
|||
outdated_only_checkbox.setToolTip("Show outdated files only")
|
||||
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.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.setToolTip("Refresh")
|
||||
refresh_button.setIcon(icon)
|
||||
refresh_button.setIcon(refresh_icon)
|
||||
|
||||
control_layout = QtWidgets.QHBoxLayout()
|
||||
control_layout.addWidget(filter_label)
|
||||
control_layout.addWidget(text_filter)
|
||||
control_layout.addWidget(outdated_only_checkbox)
|
||||
control_layout.addWidget(update_all_button)
|
||||
control_layout.addWidget(refresh_button)
|
||||
|
||||
model = InventoryModel(controller)
|
||||
proxy = FilterProxyModel()
|
||||
proxy.setSourceModel(model)
|
||||
proxy.setDynamicSortFilter(True)
|
||||
proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
|
||||
headers_widget = QtWidgets.QWidget(self)
|
||||
headers_layout = QtWidgets.QHBoxLayout(headers_widget)
|
||||
headers_layout.setContentsMargins(0, 0, 0, 0)
|
||||
headers_layout.addWidget(filter_label, 0)
|
||||
headers_layout.addWidget(text_filter, 1)
|
||||
headers_layout.addWidget(outdated_only_checkbox, 0)
|
||||
headers_layout.addWidget(update_all_button, 0)
|
||||
headers_layout.addWidget(refresh_button, 0)
|
||||
|
||||
view = SceneInventoryView(controller, self)
|
||||
view.setModel(proxy)
|
||||
|
||||
sync_enabled = controller.is_sitesync_enabled()
|
||||
view.setColumnHidden(model.active_site_col, not sync_enabled)
|
||||
view.setColumnHidden(model.remote_site_col, not sync_enabled)
|
||||
|
||||
# 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)
|
||||
main_layout = QtWidgets.QVBoxLayout(self)
|
||||
main_layout.addWidget(headers_widget, 0)
|
||||
main_layout.addWidget(view, 1)
|
||||
|
||||
show_timer = QtCore.QTimer()
|
||||
show_timer.setInterval(0)
|
||||
|
|
@ -114,12 +84,8 @@ class SceneInventoryWindow(QtWidgets.QDialog):
|
|||
self._update_all_button = update_all_button
|
||||
self._outdated_only_checkbox = outdated_only_checkbox
|
||||
self._view = view
|
||||
self._model = model
|
||||
self._proxy = proxy
|
||||
self._version_delegate = version_delegate
|
||||
|
||||
self._first_show = True
|
||||
self._first_refresh = True
|
||||
|
||||
def showEvent(self, event):
|
||||
super(SceneInventoryWindow, self).showEvent(event)
|
||||
|
|
@ -139,29 +105,16 @@ class SceneInventoryWindow(QtWidgets.QDialog):
|
|||
whilst trying to name an instance.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
def _on_refresh_request(self):
|
||||
"""Signal callback to trigger 'refresh' without any arguments."""
|
||||
|
||||
self.refresh()
|
||||
|
||||
def refresh(self, containers=None):
|
||||
self._first_refresh = False
|
||||
def refresh(self):
|
||||
self._controller.reset()
|
||||
with preserve_expanded_rows(
|
||||
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)
|
||||
self._view.refresh()
|
||||
|
||||
def _on_show_timer(self):
|
||||
if self._show_counter < 3:
|
||||
|
|
@ -171,17 +124,13 @@ class SceneInventoryWindow(QtWidgets.QDialog):
|
|||
self.refresh()
|
||||
|
||||
def _on_hierarchy_view_change(self, enabled):
|
||||
self._proxy.set_hierarchy_view(enabled)
|
||||
self._model.set_hierarchy_view(enabled)
|
||||
self._view.set_hierarchy_view(enabled)
|
||||
|
||||
def _on_text_filter_change(self, text_filter):
|
||||
if hasattr(self._proxy, "setFilterRegExp"):
|
||||
self._proxy.setFilterRegExp(text_filter)
|
||||
else:
|
||||
self._proxy.setFilterRegularExpression(text_filter)
|
||||
self._view.set_text_filter(text_filter)
|
||||
|
||||
def _on_outdated_state_change(self):
|
||||
self._proxy.set_filter_outdated(
|
||||
self._view.set_filter_outdated(
|
||||
self._outdated_only_checkbox.isChecked()
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import time
|
|||
from datetime import datetime
|
||||
import logging
|
||||
|
||||
from qtpy import QtWidgets
|
||||
from qtpy import QtWidgets, QtGui
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -106,3 +106,80 @@ class PrettyTimeDelegate(QtWidgets.QStyledItemDelegate):
|
|||
def displayText(self, value, locale):
|
||||
if value is not None:
|
||||
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 sys
|
||||
import contextlib
|
||||
import collections
|
||||
from functools import partial
|
||||
|
||||
from qtpy import QtWidgets, QtCore, QtGui
|
||||
|
|
@ -196,16 +197,16 @@ def get_openpype_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"""
|
||||
indices = [QtCore.QModelIndex()] # start iteration at root
|
||||
|
||||
for index in indices:
|
||||
indexes_queue = collections.deque()
|
||||
# start iteration at root
|
||||
indexes_queue.append(QtCore.QModelIndex())
|
||||
while indexes_queue:
|
||||
index = indexes_queue.popleft()
|
||||
# Add children to the iterations
|
||||
child_rows = model.rowCount(index)
|
||||
for child_row in range(child_rows):
|
||||
child_index = model.index(child_row, column, index)
|
||||
indices.append(child_index)
|
||||
for child_row in range(model.rowCount(index)):
|
||||
indexes_queue.append(model.index(child_row, column, index))
|
||||
|
||||
if not include_root and not index.isValid():
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -13,8 +13,10 @@ class WorkfileInfo:
|
|||
task_id (str): Task id.
|
||||
filepath (str): Filepath.
|
||||
filesize (int): File size.
|
||||
creation_time (int): Creation time (timestamp).
|
||||
modification_time (int): Modification time (timestamp).
|
||||
creation_time (float): Creation 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.
|
||||
"""
|
||||
|
||||
|
|
@ -26,6 +28,8 @@ class WorkfileInfo:
|
|||
filesize,
|
||||
creation_time,
|
||||
modification_time,
|
||||
created_by,
|
||||
updated_by,
|
||||
note,
|
||||
):
|
||||
self.folder_id = folder_id
|
||||
|
|
@ -34,6 +38,8 @@ class WorkfileInfo:
|
|||
self.filesize = filesize
|
||||
self.creation_time = creation_time
|
||||
self.modification_time = modification_time
|
||||
self.created_by = created_by
|
||||
self.updated_by = updated_by
|
||||
self.note = note
|
||||
|
||||
def to_data(self):
|
||||
|
|
@ -50,6 +56,8 @@ class WorkfileInfo:
|
|||
"filesize": self.filesize,
|
||||
"creation_time": self.creation_time,
|
||||
"modification_time": self.modification_time,
|
||||
"created_by": self.created_by,
|
||||
"updated_by": self.updated_by,
|
||||
"note": self.note,
|
||||
}
|
||||
|
||||
|
|
@ -212,6 +220,7 @@ class FileItem:
|
|||
dirpath (str): Directory path of file.
|
||||
filename (str): Filename.
|
||||
modified (float): Modified timestamp.
|
||||
created_by (Optional[str]): Username.
|
||||
representation_id (Optional[str]): Representation id of published
|
||||
workfile.
|
||||
filepath (Optional[str]): Prepared filepath.
|
||||
|
|
@ -223,6 +232,8 @@ class FileItem:
|
|||
dirpath,
|
||||
filename,
|
||||
modified,
|
||||
created_by=None,
|
||||
updated_by=None,
|
||||
representation_id=None,
|
||||
filepath=None,
|
||||
exists=None
|
||||
|
|
@ -230,6 +241,8 @@ class FileItem:
|
|||
self.filename = filename
|
||||
self.dirpath = dirpath
|
||||
self.modified = modified
|
||||
self.created_by = created_by
|
||||
self.updated_by = updated_by
|
||||
self.representation_id = representation_id
|
||||
self._filepath = filepath
|
||||
self._exists = exists
|
||||
|
|
@ -269,6 +282,7 @@ class FileItem:
|
|||
"filename": self.filename,
|
||||
"dirpath": self.dirpath,
|
||||
"modified": self.modified,
|
||||
"created_by": self.created_by,
|
||||
"representation_id": self.representation_id,
|
||||
"filepath": self.filepath,
|
||||
"exists": self.exists,
|
||||
|
|
@ -522,6 +536,16 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon):
|
|||
|
||||
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
|
||||
@abstractmethod
|
||||
def get_workfile_extensions(self):
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ from ayon_core.tools.common_models import (
|
|||
HierarchyModel,
|
||||
HierarchyExpectedSelection,
|
||||
ProjectsModel,
|
||||
UsersModel,
|
||||
)
|
||||
|
||||
from .abstract import (
|
||||
|
|
@ -161,6 +162,7 @@ class BaseWorkfileController(
|
|||
self._save_is_enabled = True
|
||||
|
||||
# Expected selected folder and task
|
||||
self._users_model = self._create_users_model()
|
||||
self._expected_selection = self._create_expected_selection_obj()
|
||||
self._selection_model = self._create_selection_model()
|
||||
self._projects_model = self._create_projects_model()
|
||||
|
|
@ -176,6 +178,12 @@ class BaseWorkfileController(
|
|||
def is_host_valid(self):
|
||||
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):
|
||||
return WorkfilesToolExpectedSelection(self)
|
||||
|
||||
|
|
@ -188,9 +196,6 @@ class BaseWorkfileController(
|
|||
def _create_hierarchy_model(self):
|
||||
return HierarchyModel(self)
|
||||
|
||||
def _create_workfiles_model(self):
|
||||
return WorkfilesModel(self)
|
||||
|
||||
@property
|
||||
def event_system(self):
|
||||
"""Inner event system for workfiles tool controller.
|
||||
|
|
@ -272,6 +277,9 @@ class BaseWorkfileController(
|
|||
{"enabled": enabled}
|
||||
)
|
||||
|
||||
def get_user_items_by_name(self):
|
||||
return self._users_model.get_user_items_by_name()
|
||||
|
||||
# Host information
|
||||
def get_workfile_extensions(self):
|
||||
host = self._host
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import arrow
|
|||
import ayon_api
|
||||
from ayon_api.operations import OperationsSession
|
||||
|
||||
from ayon_core.lib import get_ayon_username
|
||||
from ayon_core.pipeline.template_data import (
|
||||
get_template_data,
|
||||
get_task_template_data,
|
||||
|
|
@ -23,6 +24,8 @@ from ayon_core.tools.workfiles.abstract import (
|
|||
WorkfileInfo,
|
||||
)
|
||||
|
||||
_NOT_SET = object()
|
||||
|
||||
|
||||
class CommentMatcher(object):
|
||||
"""Use anatomy and work file data to parse comments from filenames.
|
||||
|
|
@ -188,10 +191,17 @@ class WorkareaModel:
|
|||
if ext not in self._extensions:
|
||||
continue
|
||||
|
||||
modified = os.path.getmtime(filepath)
|
||||
items.append(
|
||||
FileItem(workdir, filename, modified)
|
||||
workfile_info = self._controller.get_workfile_info(
|
||||
folder_id, task_id, filepath
|
||||
)
|
||||
modified = os.path.getmtime(filepath)
|
||||
items.append(FileItem(
|
||||
workdir,
|
||||
filename,
|
||||
modified,
|
||||
workfile_info.created_by,
|
||||
workfile_info.updated_by,
|
||||
))
|
||||
return items
|
||||
|
||||
def _get_template_key(self, fill_data):
|
||||
|
|
@ -439,6 +449,7 @@ class WorkfileEntitiesModel:
|
|||
self._controller = controller
|
||||
self._cache = {}
|
||||
self._items = {}
|
||||
self._current_username = _NOT_SET
|
||||
|
||||
def _get_workfile_info_identifier(
|
||||
self, folder_id, task_id, rootless_path
|
||||
|
|
@ -459,8 +470,12 @@ class WorkfileEntitiesModel:
|
|||
self, folder_id, task_id, workfile_info, filepath
|
||||
):
|
||||
note = ""
|
||||
created_by = None
|
||||
updated_by = None
|
||||
if workfile_info:
|
||||
note = workfile_info["attrib"].get("description") or ""
|
||||
created_by = workfile_info.get("createdBy")
|
||||
updated_by = workfile_info.get("updatedBy")
|
||||
|
||||
filestat = os.stat(filepath)
|
||||
return WorkfileInfo(
|
||||
|
|
@ -470,6 +485,8 @@ class WorkfileEntitiesModel:
|
|||
filesize=filestat.st_size,
|
||||
creation_time=filestat.st_ctime,
|
||||
modification_time=filestat.st_mtime,
|
||||
created_by=created_by,
|
||||
updated_by=updated_by,
|
||||
note=note
|
||||
)
|
||||
|
||||
|
|
@ -481,7 +498,7 @@ class WorkfileEntitiesModel:
|
|||
for workfile_info in ayon_api.get_workfiles_info(
|
||||
self._controller.get_current_project_name(),
|
||||
task_ids=[task_id],
|
||||
fields=["id", "path", "attrib"],
|
||||
fields=["id", "path", "attrib", "createdBy", "updatedBy"],
|
||||
):
|
||||
workfile_identifier = self._get_workfile_info_identifier(
|
||||
folder_id, task_id, workfile_info["path"]
|
||||
|
|
@ -525,18 +542,32 @@ class WorkfileEntitiesModel:
|
|||
self._items.pop(identifier, None)
|
||||
return
|
||||
|
||||
if note is None:
|
||||
return
|
||||
|
||||
old_note = workfile_info.get("attrib", {}).get("note")
|
||||
|
||||
new_workfile_info = copy.deepcopy(workfile_info)
|
||||
attrib = new_workfile_info.setdefault("attrib", {})
|
||||
attrib["description"] = note
|
||||
update_data = {}
|
||||
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._items.pop(identifier, None)
|
||||
if old_note == note:
|
||||
return
|
||||
|
||||
project_name = self._controller.get_current_project_name()
|
||||
|
||||
|
|
@ -545,7 +576,7 @@ class WorkfileEntitiesModel:
|
|||
project_name,
|
||||
"workfile",
|
||||
workfile_info["id"],
|
||||
{"attrib": {"description": note}},
|
||||
update_data,
|
||||
)
|
||||
session.commit()
|
||||
|
||||
|
|
@ -554,13 +585,18 @@ class WorkfileEntitiesModel:
|
|||
|
||||
project_name = self._controller.get_current_project_name()
|
||||
|
||||
username = self._get_current_username()
|
||||
workfile_info = {
|
||||
"path": rootless_path,
|
||||
"taskId": task_id,
|
||||
"attrib": {
|
||||
"extension": extension,
|
||||
"description": note
|
||||
}
|
||||
},
|
||||
# TODO remove 'createdBy' and 'updatedBy' fields when server is
|
||||
# or above 1.1.3 .
|
||||
"createdBy": username,
|
||||
"updatedBy": username,
|
||||
}
|
||||
|
||||
session = OperationsSession()
|
||||
|
|
@ -568,6 +604,11 @@ class WorkfileEntitiesModel:
|
|||
session.commit()
|
||||
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:
|
||||
"""Model for handling of published workfiles.
|
||||
|
|
@ -599,7 +640,7 @@ class PublishWorkfilesModel:
|
|||
return self._cached_repre_extensions
|
||||
|
||||
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:
|
||||
task_info = repre_entity["context"].get("task")
|
||||
|
|
@ -634,6 +675,8 @@ class PublishWorkfilesModel:
|
|||
dirpath,
|
||||
filename,
|
||||
created_at.float_timestamp,
|
||||
author,
|
||||
None,
|
||||
repre_entity["id"]
|
||||
)
|
||||
|
||||
|
|
@ -643,9 +686,9 @@ class PublishWorkfilesModel:
|
|||
# Get subset docs of folder
|
||||
product_entities = ayon_api.get_products(
|
||||
project_name,
|
||||
folder_ids=[folder_id],
|
||||
product_types=["workfile"],
|
||||
fields=["id", "name"]
|
||||
folder_ids={folder_id},
|
||||
product_types={"workfile"},
|
||||
fields={"id", "name"}
|
||||
)
|
||||
|
||||
output = []
|
||||
|
|
@ -657,25 +700,33 @@ class PublishWorkfilesModel:
|
|||
version_entities = ayon_api.get_versions(
|
||||
project_name,
|
||||
product_ids=product_ids,
|
||||
fields=["id", "productId"]
|
||||
fields={"id", "author"}
|
||||
)
|
||||
version_ids = {version["id"] for version in version_entities}
|
||||
if not version_ids:
|
||||
versions_by_id = {
|
||||
version["id"]: version
|
||||
for version in version_entities
|
||||
}
|
||||
if not versions_by_id:
|
||||
return output
|
||||
|
||||
# Query representations of filtered versions and add filter for
|
||||
# extension
|
||||
repre_entities = ayon_api.get_representations(
|
||||
project_name,
|
||||
version_ids=version_ids
|
||||
version_ids=set(versions_by_id)
|
||||
)
|
||||
project_anatomy = self._controller.project_anatomy
|
||||
|
||||
# Filter queried representations by task name if task is set
|
||||
file_items = []
|
||||
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(
|
||||
repre_entity, project_anatomy, task_name
|
||||
repre_entity,
|
||||
project_anatomy,
|
||||
version_entity["author"],
|
||||
task_name,
|
||||
)
|
||||
if file_item is not None:
|
||||
file_items.append(file_item)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@ from .utils import BaseOverlayFrame
|
|||
|
||||
REPRE_ID_ROLE = QtCore.Qt.UserRole + 1
|
||||
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):
|
||||
|
|
@ -23,13 +24,19 @@ class PublishedFilesModel(QtGui.QStandardItemModel):
|
|||
controller (AbstractWorkfilesFrontend): The control object.
|
||||
"""
|
||||
|
||||
columns = [
|
||||
"Name",
|
||||
"Author",
|
||||
"Date Modified",
|
||||
]
|
||||
date_modified_col = columns.index("Date Modified")
|
||||
|
||||
def __init__(self, controller):
|
||||
super(PublishedFilesModel, self).__init__()
|
||||
|
||||
self.setColumnCount(2)
|
||||
|
||||
self.setHeaderData(0, QtCore.Qt.Horizontal, "Name")
|
||||
self.setHeaderData(1, QtCore.Qt.Horizontal, "Date Modified")
|
||||
self.setColumnCount(len(self.columns))
|
||||
for idx, label in enumerate(self.columns):
|
||||
self.setHeaderData(idx, QtCore.Qt.Horizontal, label)
|
||||
|
||||
controller.register_event_callback(
|
||||
"selection.task.changed",
|
||||
|
|
@ -185,6 +192,8 @@ class PublishedFilesModel(QtGui.QStandardItemModel):
|
|||
self._remove_empty_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())
|
||||
new_items = []
|
||||
for file_item in file_items:
|
||||
|
|
@ -205,8 +214,15 @@ class PublishedFilesModel(QtGui.QStandardItemModel):
|
|||
else:
|
||||
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.setData(file_item.filepath, FILEPATH_ROLE)
|
||||
item.setData(author, AUTHOR_ROLE)
|
||||
item.setData(file_item.modified, DATE_MODIFIED_ROLE)
|
||||
|
||||
self._items_by_id[repre_id] = item
|
||||
|
|
@ -225,22 +241,30 @@ class PublishedFilesModel(QtGui.QStandardItemModel):
|
|||
# Use flags of first column for all columns
|
||||
if index.column() != 0:
|
||||
index = self.index(index.row(), 0, index.parent())
|
||||
return super(PublishedFilesModel, self).flags(index)
|
||||
return super().flags(index)
|
||||
|
||||
def data(self, index, role=None):
|
||||
if role is None:
|
||||
role = QtCore.Qt.DisplayRole
|
||||
|
||||
# Handle roles for first column
|
||||
if index.column() == 1:
|
||||
if role == QtCore.Qt.DecorationRole:
|
||||
return None
|
||||
col = index.column()
|
||||
if col != 1:
|
||||
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
|
||||
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):
|
||||
|
|
@ -295,7 +319,7 @@ class PublishedFilesWidget(QtWidgets.QWidget):
|
|||
view.setModel(proxy_model)
|
||||
|
||||
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
|
||||
# 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
|
||||
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):
|
||||
|
|
@ -21,14 +22,20 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel):
|
|||
"""
|
||||
|
||||
refreshed = QtCore.Signal()
|
||||
columns = [
|
||||
"Name",
|
||||
"Author",
|
||||
"Date Modified",
|
||||
]
|
||||
date_modified_col = columns.index("Date Modified")
|
||||
|
||||
def __init__(self, controller):
|
||||
super(WorkAreaFilesModel, self).__init__()
|
||||
|
||||
self.setColumnCount(2)
|
||||
self.setColumnCount(len(self.columns))
|
||||
|
||||
self.setHeaderData(0, QtCore.Qt.Horizontal, "Name")
|
||||
self.setHeaderData(1, QtCore.Qt.Horizontal, "Date Modified")
|
||||
for idx, label in enumerate(self.columns):
|
||||
self.setHeaderData(idx, QtCore.Qt.Horizontal, label)
|
||||
|
||||
controller.register_event_callback(
|
||||
"selection.folder.changed",
|
||||
|
|
@ -186,6 +193,7 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel):
|
|||
return
|
||||
self._remove_empty_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())
|
||||
new_items = []
|
||||
|
|
@ -205,7 +213,13 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel):
|
|||
item.setData(file_item.filename, QtCore.Qt.DisplayRole)
|
||||
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(updated_by, AUTHOR_ROLE)
|
||||
item.setData(file_item.modified, DATE_MODIFIED_ROLE)
|
||||
|
||||
self._items_by_filename[file_item.filename] = item
|
||||
|
|
@ -224,22 +238,30 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel):
|
|||
# Use flags of first column for all columns
|
||||
if index.column() != 0:
|
||||
index = self.index(index.row(), 0, index.parent())
|
||||
return super(WorkAreaFilesModel, self).flags(index)
|
||||
return super().flags(index)
|
||||
|
||||
def data(self, index, role=None):
|
||||
if role is None:
|
||||
role = QtCore.Qt.DisplayRole
|
||||
|
||||
# Handle roles for first column
|
||||
if index.column() == 1:
|
||||
if role == QtCore.Qt.DecorationRole:
|
||||
return None
|
||||
col = index.column()
|
||||
if col == 0:
|
||||
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
|
||||
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):
|
||||
if self._published_mode == published_mode:
|
||||
|
|
@ -279,7 +301,7 @@ class WorkAreaFilesWidget(QtWidgets.QWidget):
|
|||
view.setModel(proxy_model)
|
||||
|
||||
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
|
||||
# about and the date modified is relatively small anyway.
|
||||
|
|
|
|||
|
|
@ -147,13 +147,38 @@ class SidePanelWidget(QtWidgets.QWidget):
|
|||
workfile_info.creation_time)
|
||||
modification_time = datetime.datetime.fromtimestamp(
|
||||
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 = (
|
||||
"<b>Size:</b>",
|
||||
size_value,
|
||||
"<b>Created:</b>",
|
||||
creation_time.strftime(datetime_format),
|
||||
"<br/>".join(created_lines),
|
||||
"<b>Modified:</b>",
|
||||
modification_time.strftime(datetime_format)
|
||||
"<br/>".join(modified_lines),
|
||||
)
|
||||
self._orig_note = note
|
||||
self._note_input.setPlainText(note)
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ class WorkfilesToolWindow(QtWidgets.QWidget):
|
|||
split_widget.addWidget(tasks_widget)
|
||||
split_widget.addWidget(col_3_widget)
|
||||
split_widget.addWidget(side_panel)
|
||||
split_widget.setSizes([255, 160, 455, 175])
|
||||
split_widget.setSizes([255, 175, 550, 190])
|
||||
|
||||
body_layout.addWidget(split_widget)
|
||||
|
||||
|
|
@ -169,7 +169,7 @@ class WorkfilesToolWindow(QtWidgets.QWidget):
|
|||
# Force focus on the open button by default, required for Houdini.
|
||||
self._files_widget.setFocus()
|
||||
|
||||
self.resize(1200, 600)
|
||||
self.resize(1260, 600)
|
||||
|
||||
def _create_col_1_widget(self, controller, parent):
|
||||
col_widget = QtWidgets.QWidget(parent)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring AYON core addon version."""
|
||||
__version__ = "0.3.1-dev.1"
|
||||
__version__ = "0.3.3-dev.1"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ aiohttp_json_rpc = "*" # TVPaint server
|
|||
aiohttp-middlewares = "^2.0.0"
|
||||
wsrpc_aiohttp = "^3.1.1" # websocket server
|
||||
Click = "^8"
|
||||
OpenTimelineIO = "0.14.1"
|
||||
OpenTimelineIO = "0.16.0"
|
||||
opencolorio = "2.2.1"
|
||||
Pillow = "9.5.0"
|
||||
pynput = "^1.7.2" # Timers manager - TODO remove
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
name = "core"
|
||||
title = "Core"
|
||||
version = "0.3.1-dev.1"
|
||||
version = "0.3.3-dev.1"
|
||||
|
||||
client_dir = "ayon_core"
|
||||
|
||||
plugin_for = ["ayon_server"]
|
||||
requires = [
|
||||
"~ayon_server-1.0.3+<2.0.0",
|
||||
]
|
||||
|
||||
ayon_server_version = ">=1.0.3,<2.0.0"
|
||||
ayon_launcher_version = ">=1.0.2"
|
||||
ayon_required_addons = {}
|
||||
ayon_compatible_addons = {}
|
||||
|
|
|
|||
|
|
@ -108,6 +108,10 @@ line-ending = "auto"
|
|||
# Ignore words that are not in the dictionary.
|
||||
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/*"
|
||||
count = true
|
||||
quiet-level = 3
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
from typing import Any
|
||||
|
||||
from ayon_server.addons import BaseServerAddon
|
||||
|
||||
from .settings import CoreSettings, DEFAULT_VALUES
|
||||
from .settings import (
|
||||
CoreSettings,
|
||||
DEFAULT_VALUES,
|
||||
convert_settings_overrides,
|
||||
)
|
||||
|
||||
|
||||
class CoreAddon(BaseServerAddon):
|
||||
|
|
@ -9,3 +15,14 @@ class CoreAddon(BaseServerAddon):
|
|||
async def get_default_settings(self):
|
||||
settings_model_cls = self.get_settings_model()
|
||||
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 .conversion import convert_settings_overrides
|
||||
|
||||
|
||||
__all__ = (
|
||||
"CoreSettings",
|
||||
"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
|
||||
|
||||
|
||||
class CoreImageIOConfigModel(BaseSettingsModel):
|
||||
filepath: list[str] = SettingsField(
|
||||
default_factory=list, title="Config path"
|
||||
def _ocio_config_profile_types():
|
||||
return [
|
||||
{"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,
|
||||
title="Enable Color Management"
|
||||
)
|
||||
ocio_config: CoreImageIOConfigModel = SettingsField(
|
||||
default_factory=CoreImageIOConfigModel,
|
||||
title="OCIO config"
|
||||
ocio_config_profiles: list[CoreImageIOConfigProfilesModel] = SettingsField(
|
||||
default_factory=list, title="OCIO config profiles"
|
||||
)
|
||||
file_rules: CoreImageIOFileRulesModel = SettingsField(
|
||||
default_factory=CoreImageIOFileRulesModel,
|
||||
|
|
@ -186,12 +243,17 @@ class CoreSettings(BaseSettingsModel):
|
|||
DEFAULT_VALUES = {
|
||||
"imageio": {
|
||||
"activate_global_color_management": False,
|
||||
"ocio_config": {
|
||||
"filepath": [
|
||||
"{BUILTIN_OCIO_ROOT}/aces_1.2/config.ocio",
|
||||
"{BUILTIN_OCIO_ROOT}/nuke-default/config.ocio"
|
||||
]
|
||||
},
|
||||
"ocio_config_profiles": [
|
||||
{
|
||||
"host_names": [],
|
||||
"task_types": [],
|
||||
"task_names": [],
|
||||
"type": "builtin_path",
|
||||
"builtin_path": "{BUILTIN_OCIO_ROOT}/aces_1.2/config.ocio",
|
||||
"custom_path": "",
|
||||
"product_name": "",
|
||||
}
|
||||
],
|
||||
"file_rules": {
|
||||
"activate_global_file_rules": False,
|
||||
"rules": [
|
||||
|
|
@ -199,42 +261,57 @@ DEFAULT_VALUES = {
|
|||
"name": "example",
|
||||
"pattern": ".*(beauty).*",
|
||||
"colorspace": "ACES - ACEScg",
|
||||
"ext": "exr"
|
||||
"ext": "exr",
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
},
|
||||
},
|
||||
"studio_name": "",
|
||||
"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,
|
||||
"version_start_category": {
|
||||
"profiles": []
|
||||
},
|
||||
"publish": DEFAULT_PUBLISH_VALUES,
|
||||
"project_folder_structure": json.dumps({
|
||||
"__project_root__": {
|
||||
"prod": {},
|
||||
"resources": {
|
||||
"footage": {
|
||||
"plates": {},
|
||||
"offline": {}
|
||||
"project_folder_structure": json.dumps(
|
||||
{
|
||||
"__project_root__": {
|
||||
"prod": {},
|
||||
"resources": {
|
||||
"footage": {
|
||||
"plates": {},
|
||||
"offline": {}
|
||||
},
|
||||
"audio": {},
|
||||
"art_dept": {}
|
||||
},
|
||||
"audio": {},
|
||||
"art_dept": {}
|
||||
},
|
||||
"editorial": {},
|
||||
"assets": {
|
||||
"characters": {},
|
||||
"locations": {}
|
||||
},
|
||||
"shots": {}
|
||||
}
|
||||
}, indent=4),
|
||||
"editorial": {},
|
||||
"assets": {
|
||||
"characters": {},
|
||||
"locations": {}
|
||||
},
|
||||
"shots": {}
|
||||
}
|
||||
},
|
||||
indent=4
|
||||
),
|
||||
"project_plugins": {
|
||||
"windows": [],
|
||||
"darwin": [],
|
||||
"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):
|
||||
_layout = "expanded"
|
||||
hosts: list[str] = SettingsField(default_factory=list, title="Host names")
|
||||
|
|
@ -536,7 +558,7 @@ class ExtractBurninProfile(BaseSettingsModel):
|
|||
_layout = "expanded"
|
||||
product_types: list[str] = SettingsField(
|
||||
default_factory=list,
|
||||
title="Produt types"
|
||||
title="Product types"
|
||||
)
|
||||
hosts: list[str] = SettingsField(
|
||||
default_factory=list,
|
||||
|
|
@ -766,9 +788,17 @@ class PublishPuginsModel(BaseSettingsModel):
|
|||
default_factory=ValidateBaseModel,
|
||||
title="Validate Editorial Asset Name"
|
||||
)
|
||||
ValidateVersion: ValidateBaseModel = SettingsField(
|
||||
default_factory=ValidateBaseModel,
|
||||
title="Validate Version"
|
||||
ValidateVersion: PluginStateByHostModel = SettingsField(
|
||||
default_factory=PluginStateByHostModel,
|
||||
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(
|
||||
default_factory=ValidateIntentModel,
|
||||
|
|
@ -851,9 +881,40 @@ DEFAULT_PUBLISH_VALUES = {
|
|||
"active": True
|
||||
},
|
||||
"ValidateVersion": {
|
||||
"enabled": True,
|
||||
"optional": False,
|
||||
"active": True
|
||||
"plugin_state_profiles": [
|
||||
{
|
||||
"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": {
|
||||
"enabled": False,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
name = "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):
|
||||
CollectReview: CollectReviewPluginModel = SettingsField(
|
||||
default_factory=CollectReviewPluginModel,
|
||||
|
|
@ -37,10 +31,6 @@ class AfterEffectsPublishPlugins(BaseSettingsModel):
|
|||
default_factory=ValidateSceneSettingsModel,
|
||||
title="Validate Scene Settings",
|
||||
)
|
||||
ValidateContainers: ValidateContainersModel = SettingsField(
|
||||
default_factory=ValidateContainersModel,
|
||||
title="Validate Containers",
|
||||
)
|
||||
|
||||
|
||||
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"]
|
||||
)
|
||||
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")
|
||||
def validate_unique_name(cls, value):
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
name = "blender"
|
||||
title = "Blender"
|
||||
version = "0.1.8"
|
||||
version = "0.1.9"
|
||||
|
|
|
|||
|
|
@ -151,6 +151,10 @@ class PublishPluginsModel(BaseSettingsModel):
|
|||
default_factory=ExtractPlayblastModel,
|
||||
title="Extract Playblast"
|
||||
)
|
||||
ExtractModelUSD: ValidatePluginModel = SettingsField(
|
||||
default_factory=ValidatePluginModel,
|
||||
title="Extract Model USD"
|
||||
)
|
||||
|
||||
|
||||
DEFAULT_BLENDER_PUBLISH_SETTINGS = {
|
||||
|
|
@ -348,5 +352,10 @@ DEFAULT_BLENDER_PUBLISH_SETTINGS = {
|
|||
},
|
||||
indent=4
|
||||
)
|
||||
},
|
||||
"ExtractModelUSD": {
|
||||
"enabled": True,
|
||||
"optional": True,
|
||||
"active": True
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from .version import __version__
|
||||
from .addon import (
|
||||
CELACTION_ROOT_DIR,
|
||||
CelactionAddon,
|
||||
|
|
@ -5,6 +6,8 @@ from .addon import (
|
|||
|
||||
|
||||
__all__ = (
|
||||
"__version__",
|
||||
|
||||
"CELACTION_ROOT_DIR",
|
||||
"CelactionAddon",
|
||||
)
|
||||
|
|
@ -1,11 +1,14 @@
|
|||
import os
|
||||
from ayon_core.addon import AYONAddon, IHostAddon
|
||||
|
||||
from .version import __version__
|
||||
|
||||
CELACTION_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
class CelactionAddon(AYONAddon, IHostAddon):
|
||||
name = "celaction"
|
||||
version = __version__
|
||||
host_name = "celaction"
|
||||
|
||||
def get_launch_hook_paths(self, app):
|
||||
|
|
@ -4,13 +4,11 @@ import winreg
|
|||
import subprocess
|
||||
from ayon_core.lib import get_ayon_launcher_args
|
||||
from ayon_applications import PreLaunchHook, LaunchTypes
|
||||
from ayon_core.hosts.celaction import CELACTION_ROOT_DIR
|
||||
from ayon_celaction import CELACTION_ROOT_DIR
|
||||
|
||||
|
||||
class CelactionPrelaunchHook(PreLaunchHook):
|
||||
"""
|
||||
Bootstrap celacion with pype
|
||||
"""
|
||||
"""Bootstrap celacion with AYON"""
|
||||
app_groups = {"celaction"}
|
||||
platforms = {"windows"}
|
||||
launch_types = {LaunchTypes.local}
|
||||
|
|
@ -39,7 +37,7 @@ class CelactionPrelaunchHook(PreLaunchHook):
|
|||
CELACTION_ROOT_DIR, "scripts", "publish_cli.py"
|
||||
)
|
||||
subprocess_args = get_ayon_launcher_args("run", path_to_cli)
|
||||
openpype_executable = subprocess_args.pop(0)
|
||||
executable = subprocess_args.pop(0)
|
||||
workfile_settings = self.get_workfile_settings()
|
||||
|
||||
winreg.SetValueEx(
|
||||
|
|
@ -47,7 +45,7 @@ class CelactionPrelaunchHook(PreLaunchHook):
|
|||
"SubmitAppTitle",
|
||||
0,
|
||||
winreg.REG_SZ,
|
||||
openpype_executable
|
||||
executable
|
||||
)
|
||||
|
||||
# add required arguments for workfile path
|
||||
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