mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 12:54:40 +01:00
Merge pull request #1301 from ynput/enhancement/1294-product-base-types-support-in-loading
🏛️Product base types: add support to loaders
This commit is contained in:
commit
b2ab8ef5c2
6 changed files with 386 additions and 101 deletions
|
|
@ -1,24 +1,28 @@
|
||||||
|
"""Plugins for loading representations and products into host applications."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import os
|
|
||||||
import logging
|
|
||||||
from typing import Any, Type, Optional
|
|
||||||
from abc import abstractmethod
|
|
||||||
|
|
||||||
from ayon_core.settings import get_project_settings
|
from abc import abstractmethod
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from typing import Any, Optional, Type
|
||||||
|
|
||||||
from ayon_core.pipeline.plugin_discover import (
|
from ayon_core.pipeline.plugin_discover import (
|
||||||
|
deregister_plugin,
|
||||||
|
deregister_plugin_path,
|
||||||
discover,
|
discover,
|
||||||
register_plugin,
|
register_plugin,
|
||||||
register_plugin_path,
|
register_plugin_path,
|
||||||
deregister_plugin,
|
|
||||||
deregister_plugin_path
|
|
||||||
)
|
)
|
||||||
|
from ayon_core.settings import get_project_settings
|
||||||
|
|
||||||
from .utils import get_representation_path_from_context
|
from .utils import get_representation_path_from_context
|
||||||
|
|
||||||
|
|
||||||
class LoaderPlugin(list):
|
class LoaderPlugin(list):
|
||||||
"""Load representation into host application"""
|
"""Load representation into host application"""
|
||||||
|
|
||||||
product_types = set()
|
product_types: set[str] = set()
|
||||||
|
product_base_types: Optional[set[str]] = None
|
||||||
representations = set()
|
representations = set()
|
||||||
extensions = {"*"}
|
extensions = {"*"}
|
||||||
order = 0
|
order = 0
|
||||||
|
|
@ -61,12 +65,12 @@ class LoaderPlugin(list):
|
||||||
if not plugin_settings:
|
if not plugin_settings:
|
||||||
return
|
return
|
||||||
|
|
||||||
print(">>> We have preset for {}".format(plugin_name))
|
print(f">>> We have preset for {plugin_name}")
|
||||||
for option, value in plugin_settings.items():
|
for option, value in plugin_settings.items():
|
||||||
if option == "enabled" and value is False:
|
if option == "enabled" and value is False:
|
||||||
print(" - is disabled by preset")
|
print(" - is disabled by preset")
|
||||||
else:
|
else:
|
||||||
print(" - setting `{}`: `{}`".format(option, value))
|
print(f" - setting `{option}`: `{value}`")
|
||||||
setattr(cls, option, value)
|
setattr(cls, option, value)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
@ -79,7 +83,6 @@ class LoaderPlugin(list):
|
||||||
Returns:
|
Returns:
|
||||||
bool: Representation has valid extension
|
bool: Representation has valid extension
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if "*" in cls.extensions:
|
if "*" in cls.extensions:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
@ -124,18 +127,34 @@ class LoaderPlugin(list):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
plugin_repre_names = cls.get_representations()
|
plugin_repre_names = cls.get_representations()
|
||||||
plugin_product_types = cls.product_types
|
|
||||||
|
# If the product base type isn't defined on the loader plugin,
|
||||||
|
# then we will use the product types.
|
||||||
|
plugin_product_filter = cls.product_base_types
|
||||||
|
if plugin_product_filter is None:
|
||||||
|
plugin_product_filter = cls.product_types
|
||||||
|
|
||||||
|
if plugin_product_filter:
|
||||||
|
plugin_product_filter = set(plugin_product_filter)
|
||||||
|
|
||||||
|
repre_entity = context.get("representation")
|
||||||
|
product_entity = context["product"]
|
||||||
|
|
||||||
|
# If no representation names, product types or extensions are defined
|
||||||
|
# then loader is not compatible with any context.
|
||||||
if (
|
if (
|
||||||
not plugin_repre_names
|
not plugin_repre_names
|
||||||
or not plugin_product_types
|
or not plugin_product_filter
|
||||||
or not cls.extensions
|
or not cls.extensions
|
||||||
):
|
):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
repre_entity = context.get("representation")
|
# If no representation entity is provided then loader is not
|
||||||
|
# compatible with context.
|
||||||
if not repre_entity:
|
if not repre_entity:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Check the compatibility with the representation names.
|
||||||
plugin_repre_names = set(plugin_repre_names)
|
plugin_repre_names = set(plugin_repre_names)
|
||||||
if (
|
if (
|
||||||
"*" not in plugin_repre_names
|
"*" not in plugin_repre_names
|
||||||
|
|
@ -143,17 +162,34 @@ class LoaderPlugin(list):
|
||||||
):
|
):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Check the compatibility with the extension of the representation.
|
||||||
if not cls.has_valid_extension(repre_entity):
|
if not cls.has_valid_extension(repre_entity):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
plugin_product_types = set(plugin_product_types)
|
product_type = product_entity.get("productType")
|
||||||
if "*" in plugin_product_types:
|
product_base_type = product_entity.get("productBaseType")
|
||||||
|
|
||||||
|
# Use product base type if defined, otherwise use product type.
|
||||||
|
product_filter = product_base_type
|
||||||
|
# If there is no product base type defined in the product entity,
|
||||||
|
# then we will use the product type.
|
||||||
|
if product_filter is None:
|
||||||
|
product_filter = product_type
|
||||||
|
|
||||||
|
# If wildcard is used in product types or base types,
|
||||||
|
# then we will consider the loader compatible with any product type.
|
||||||
|
if "*" in plugin_product_filter:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
product_entity = context["product"]
|
# compatibility with legacy loader
|
||||||
product_type = product_entity["productType"]
|
if cls.product_base_types is None and product_base_type:
|
||||||
|
cls.log.error(
|
||||||
|
f"Loader {cls.__name__} is doesn't specify "
|
||||||
|
"`product_base_types` but product entity has "
|
||||||
|
f"`productBaseType` defined as `{product_base_type}`. "
|
||||||
|
)
|
||||||
|
|
||||||
return product_type in plugin_product_types
|
return product_filter in plugin_product_filter
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_representations(cls):
|
def get_representations(cls):
|
||||||
|
|
@ -208,19 +244,17 @@ class LoaderPlugin(list):
|
||||||
bool: Whether the container was deleted
|
bool: Whether the container was deleted
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
raise NotImplementedError("Loader.remove() must be "
|
raise NotImplementedError("Loader.remove() must be "
|
||||||
"implemented by subclass")
|
"implemented by subclass")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_options(cls, contexts):
|
def get_options(cls, contexts):
|
||||||
"""
|
"""Returns static (cls) options or could collect from 'contexts'.
|
||||||
Returns static (cls) options or could collect from 'contexts'.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
contexts (list): of repre or product contexts
|
contexts (list): of repre or product contexts
|
||||||
Returns:
|
Returns:
|
||||||
(list)
|
(list)
|
||||||
"""
|
"""
|
||||||
return cls.options or []
|
return cls.options or []
|
||||||
|
|
||||||
|
|
@ -347,10 +381,8 @@ def discover_loader_plugins(project_name=None):
|
||||||
plugin.apply_settings(project_settings)
|
plugin.apply_settings(project_settings)
|
||||||
except Exception:
|
except Exception:
|
||||||
log.warning(
|
log.warning(
|
||||||
"Failed to apply settings to loader {}".format(
|
f"Failed to apply settings to loader {plugin.__name__}",
|
||||||
plugin.__name__
|
exc_info=True
|
||||||
),
|
|
||||||
exc_info=True,
|
|
||||||
)
|
)
|
||||||
compatible_hooks = []
|
compatible_hooks = []
|
||||||
for hook_cls in sorted_hooks:
|
for hook_cls in sorted_hooks:
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
|
"""Abstract base classes for loader tool."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import List
|
from typing import Any, List, Optional
|
||||||
|
|
||||||
from ayon_core.lib.attribute_definitions import (
|
from ayon_core.lib.attribute_definitions import (
|
||||||
AbstractAttrDef,
|
AbstractAttrDef,
|
||||||
serialize_attr_defs,
|
|
||||||
deserialize_attr_defs,
|
deserialize_attr_defs,
|
||||||
|
serialize_attr_defs,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -13,10 +16,10 @@ class ProductTypeItem:
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
name (str): Product type name.
|
name (str): Product type name.
|
||||||
icon (dict[str, Any]): Product type icon definition.
|
icon (dict[str, str]): Product type icon definition.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, name, icon):
|
def __init__(self, name: str, icon: dict[str, str]):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.icon = icon
|
self.icon = icon
|
||||||
|
|
||||||
|
|
@ -31,6 +34,41 @@ class ProductTypeItem:
|
||||||
return cls(**data)
|
return cls(**data)
|
||||||
|
|
||||||
|
|
||||||
|
class ProductBaseTypeItem:
|
||||||
|
"""Item representing the product base type."""
|
||||||
|
|
||||||
|
def __init__(self, name: str, icon: dict[str, str]):
|
||||||
|
"""Initialize product base type item."""
|
||||||
|
self.name = name
|
||||||
|
self.icon = icon
|
||||||
|
|
||||||
|
def to_data(self) -> dict[str, Any]:
|
||||||
|
"""Convert item to data dictionary.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict[str, Any]: Data representation of the item.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"name": self.name,
|
||||||
|
"icon": self.icon,
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_data(
|
||||||
|
cls, data: dict[str, Any]) -> ProductBaseTypeItem:
|
||||||
|
"""Create item from data dictionary.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data (dict[str, Any]): Data to create item from.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ProductBaseTypeItem: Item created from the provided data.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return cls(**data)
|
||||||
|
|
||||||
|
|
||||||
class ProductItem:
|
class ProductItem:
|
||||||
"""Product item with it versions.
|
"""Product item with it versions.
|
||||||
|
|
||||||
|
|
@ -38,8 +76,8 @@ class ProductItem:
|
||||||
product_id (str): Product id.
|
product_id (str): Product id.
|
||||||
product_type (str): Product type.
|
product_type (str): Product type.
|
||||||
product_name (str): Product name.
|
product_name (str): Product name.
|
||||||
product_icon (dict[str, Any]): Product icon definition.
|
product_icon (dict[str, str]): Product icon definition.
|
||||||
product_type_icon (dict[str, Any]): Product type icon definition.
|
product_type_icon (dict[str, str]): Product type icon definition.
|
||||||
product_in_scene (bool): Is product in scene (only when used in DCC).
|
product_in_scene (bool): Is product in scene (only when used in DCC).
|
||||||
group_name (str): Group name.
|
group_name (str): Group name.
|
||||||
folder_id (str): Folder id.
|
folder_id (str): Folder id.
|
||||||
|
|
@ -49,35 +87,41 @@ class ProductItem:
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
product_id,
|
product_id: str,
|
||||||
product_type,
|
product_type: str,
|
||||||
product_name,
|
product_base_type: str,
|
||||||
product_icon,
|
product_name: str,
|
||||||
product_type_icon,
|
product_icon: dict[str, str],
|
||||||
product_in_scene,
|
product_type_icon: dict[str, str],
|
||||||
group_name,
|
product_base_type_icon: dict[str, str],
|
||||||
folder_id,
|
group_name: str,
|
||||||
folder_label,
|
folder_id: str,
|
||||||
version_items,
|
folder_label: str,
|
||||||
|
version_items: dict[str, VersionItem],
|
||||||
|
product_in_scene: bool,
|
||||||
):
|
):
|
||||||
self.product_id = product_id
|
self.product_id = product_id
|
||||||
self.product_type = product_type
|
self.product_type = product_type
|
||||||
|
self.product_base_type = product_base_type
|
||||||
self.product_name = product_name
|
self.product_name = product_name
|
||||||
self.product_icon = product_icon
|
self.product_icon = product_icon
|
||||||
self.product_type_icon = product_type_icon
|
self.product_type_icon = product_type_icon
|
||||||
|
self.product_base_type_icon = product_base_type_icon
|
||||||
self.product_in_scene = product_in_scene
|
self.product_in_scene = product_in_scene
|
||||||
self.group_name = group_name
|
self.group_name = group_name
|
||||||
self.folder_id = folder_id
|
self.folder_id = folder_id
|
||||||
self.folder_label = folder_label
|
self.folder_label = folder_label
|
||||||
self.version_items = version_items
|
self.version_items = version_items
|
||||||
|
|
||||||
def to_data(self):
|
def to_data(self) -> dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
"product_id": self.product_id,
|
"product_id": self.product_id,
|
||||||
"product_type": self.product_type,
|
"product_type": self.product_type,
|
||||||
|
"product_base_type": self.product_base_type,
|
||||||
"product_name": self.product_name,
|
"product_name": self.product_name,
|
||||||
"product_icon": self.product_icon,
|
"product_icon": self.product_icon,
|
||||||
"product_type_icon": self.product_type_icon,
|
"product_type_icon": self.product_type_icon,
|
||||||
|
"product_base_type_icon": self.product_base_type_icon,
|
||||||
"product_in_scene": self.product_in_scene,
|
"product_in_scene": self.product_in_scene,
|
||||||
"group_name": self.group_name,
|
"group_name": self.group_name,
|
||||||
"folder_id": self.folder_id,
|
"folder_id": self.folder_id,
|
||||||
|
|
@ -124,21 +168,21 @@ class VersionItem:
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
version_id,
|
version_id: str,
|
||||||
version,
|
version: int,
|
||||||
is_hero,
|
is_hero: bool,
|
||||||
product_id,
|
product_id: str,
|
||||||
task_id,
|
task_id: Optional[str],
|
||||||
thumbnail_id,
|
thumbnail_id: Optional[str],
|
||||||
published_time,
|
published_time: Optional[str],
|
||||||
author,
|
author: Optional[str],
|
||||||
status,
|
status: Optional[str],
|
||||||
frame_range,
|
frame_range: Optional[str],
|
||||||
duration,
|
duration: Optional[int],
|
||||||
handles,
|
handles: Optional[str],
|
||||||
step,
|
step: Optional[int],
|
||||||
comment,
|
comment: Optional[str],
|
||||||
source,
|
source: Optional[str],
|
||||||
):
|
):
|
||||||
self.version_id = version_id
|
self.version_id = version_id
|
||||||
self.product_id = product_id
|
self.product_id = product_id
|
||||||
|
|
@ -198,7 +242,7 @@ class VersionItem:
|
||||||
def __le__(self, other):
|
def __le__(self, other):
|
||||||
return self.__eq__(other) or self.__lt__(other)
|
return self.__eq__(other) or self.__lt__(other)
|
||||||
|
|
||||||
def to_data(self):
|
def to_data(self) -> dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
"version_id": self.version_id,
|
"version_id": self.version_id,
|
||||||
"product_id": self.product_id,
|
"product_id": self.product_id,
|
||||||
|
|
@ -218,7 +262,7 @@ class VersionItem:
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_data(cls, data):
|
def from_data(cls, data: dict[str, Any]) -> VersionItem:
|
||||||
return cls(**data)
|
return cls(**data)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,29 @@
|
||||||
|
"""Products model for loader tools."""
|
||||||
|
from __future__ import annotations
|
||||||
import collections
|
import collections
|
||||||
import contextlib
|
import contextlib
|
||||||
|
from typing import TYPE_CHECKING, Iterable, Optional
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import ayon_api
|
import ayon_api
|
||||||
from ayon_api.operations import OperationsSession
|
from ayon_api.operations import OperationsSession
|
||||||
|
|
||||||
|
|
||||||
from ayon_core.lib import NestedCacheItem
|
from ayon_core.lib import NestedCacheItem
|
||||||
from ayon_core.style import get_default_entity_icon_color
|
from ayon_core.style import get_default_entity_icon_color
|
||||||
from ayon_core.tools.loader.abstract import (
|
from ayon_core.tools.loader.abstract import (
|
||||||
|
IconData,
|
||||||
ProductTypeItem,
|
ProductTypeItem,
|
||||||
|
ProductBaseTypeItem,
|
||||||
ProductItem,
|
ProductItem,
|
||||||
VersionItem,
|
VersionItem,
|
||||||
RepreItem,
|
RepreItem,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ayon_api.typing import ProductBaseTypeDict, ProductDict, VersionDict
|
||||||
|
|
||||||
|
|
||||||
PRODUCTS_MODEL_SENDER = "products.model"
|
PRODUCTS_MODEL_SENDER = "products.model"
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -70,9 +80,10 @@ def version_item_from_entity(version):
|
||||||
|
|
||||||
|
|
||||||
def product_item_from_entity(
|
def product_item_from_entity(
|
||||||
product_entity,
|
product_entity: ProductDict,
|
||||||
version_entities,
|
version_entities,
|
||||||
product_type_items_by_name,
|
product_type_items_by_name: dict[str, ProductTypeItem],
|
||||||
|
product_base_type_items_by_name: dict[str, ProductBaseTypeItem],
|
||||||
folder_label,
|
folder_label,
|
||||||
product_in_scene,
|
product_in_scene,
|
||||||
):
|
):
|
||||||
|
|
@ -88,9 +99,21 @@ def product_item_from_entity(
|
||||||
# Cache the item for future use
|
# Cache the item for future use
|
||||||
product_type_items_by_name[product_type] = product_type_item
|
product_type_items_by_name[product_type] = product_type_item
|
||||||
|
|
||||||
product_type_icon = product_type_item.icon
|
product_base_type = product_entity.get("productBaseType")
|
||||||
|
product_base_type_item = product_base_type_items_by_name.get(
|
||||||
|
product_base_type)
|
||||||
|
# Same as for product type item above. Not sure if this is still needed
|
||||||
|
# though.
|
||||||
|
if product_base_type_item is None:
|
||||||
|
product_base_type_item = create_default_product_base_type_item(
|
||||||
|
product_base_type)
|
||||||
|
# Cache the item for future use
|
||||||
|
product_base_type_items_by_name[product_base_type] = (
|
||||||
|
product_base_type_item)
|
||||||
|
|
||||||
product_icon = {
|
product_type_icon = product_type_item.icon
|
||||||
|
product_base_type_icon = product_base_type_item.icon
|
||||||
|
product_icon: IconData = {
|
||||||
"type": "awesome-font",
|
"type": "awesome-font",
|
||||||
"name": "fa.file-o",
|
"name": "fa.file-o",
|
||||||
"color": get_default_entity_icon_color(),
|
"color": get_default_entity_icon_color(),
|
||||||
|
|
@ -103,9 +126,11 @@ def product_item_from_entity(
|
||||||
return ProductItem(
|
return ProductItem(
|
||||||
product_id=product_entity["id"],
|
product_id=product_entity["id"],
|
||||||
product_type=product_type,
|
product_type=product_type,
|
||||||
|
product_base_type=product_base_type,
|
||||||
product_name=product_entity["name"],
|
product_name=product_entity["name"],
|
||||||
product_icon=product_icon,
|
product_icon=product_icon,
|
||||||
product_type_icon=product_type_icon,
|
product_type_icon=product_type_icon,
|
||||||
|
product_base_type_icon=product_base_type_icon,
|
||||||
product_in_scene=product_in_scene,
|
product_in_scene=product_in_scene,
|
||||||
group_name=group,
|
group_name=group,
|
||||||
folder_id=product_entity["folderId"],
|
folder_id=product_entity["folderId"],
|
||||||
|
|
@ -114,11 +139,12 @@ def product_item_from_entity(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def product_type_item_from_data(product_type_data):
|
def product_type_item_from_data(
|
||||||
|
product_type_data: ProductDict) -> ProductTypeItem:
|
||||||
# TODO implement icon implementation
|
# TODO implement icon implementation
|
||||||
# icon = product_type_data["icon"]
|
# icon = product_type_data["icon"]
|
||||||
# color = product_type_data["color"]
|
# color = product_type_data["color"]
|
||||||
icon = {
|
icon: IconData = {
|
||||||
"type": "awesome-font",
|
"type": "awesome-font",
|
||||||
"name": "fa.folder",
|
"name": "fa.folder",
|
||||||
"color": "#0091B2",
|
"color": "#0091B2",
|
||||||
|
|
@ -127,8 +153,30 @@ def product_type_item_from_data(product_type_data):
|
||||||
return ProductTypeItem(product_type_data["name"], icon)
|
return ProductTypeItem(product_type_data["name"], icon)
|
||||||
|
|
||||||
|
|
||||||
def create_default_product_type_item(product_type):
|
def product_base_type_item_from_data(
|
||||||
icon = {
|
product_base_type_data: ProductBaseTypeDict
|
||||||
|
) -> ProductBaseTypeItem:
|
||||||
|
"""Create product base type item from data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
product_base_type_data (ProductBaseTypeDict): Product base type data.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ProductBaseTypeDict: Product base type item.
|
||||||
|
|
||||||
|
"""
|
||||||
|
icon: IconData = {
|
||||||
|
"type": "awesome-font",
|
||||||
|
"name": "fa.folder",
|
||||||
|
"color": "#0091B2",
|
||||||
|
}
|
||||||
|
return ProductBaseTypeItem(
|
||||||
|
name=product_base_type_data["name"],
|
||||||
|
icon=icon)
|
||||||
|
|
||||||
|
|
||||||
|
def create_default_product_type_item(product_type: str) -> ProductTypeItem:
|
||||||
|
icon: IconData = {
|
||||||
"type": "awesome-font",
|
"type": "awesome-font",
|
||||||
"name": "fa.folder",
|
"name": "fa.folder",
|
||||||
"color": "#0091B2",
|
"color": "#0091B2",
|
||||||
|
|
@ -136,10 +184,28 @@ def create_default_product_type_item(product_type):
|
||||||
return ProductTypeItem(product_type, icon)
|
return ProductTypeItem(product_type, icon)
|
||||||
|
|
||||||
|
|
||||||
|
def create_default_product_base_type_item(
|
||||||
|
product_base_type: str) -> ProductBaseTypeItem:
|
||||||
|
"""Create default product base type item.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
product_base_type (str): Product base type name.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ProductBaseTypeItem: Default product base type item.
|
||||||
|
"""
|
||||||
|
icon: IconData = {
|
||||||
|
"type": "awesome-font",
|
||||||
|
"name": "fa.folder",
|
||||||
|
"color": "#0091B2",
|
||||||
|
}
|
||||||
|
return ProductBaseTypeItem(product_base_type, icon)
|
||||||
|
|
||||||
|
|
||||||
class ProductsModel:
|
class ProductsModel:
|
||||||
"""Model for products, version and representation.
|
"""Model for products, version and representation.
|
||||||
|
|
||||||
All of the entities are product based. This model prepares data for UI
|
All the entities are product based. This model prepares data for UI
|
||||||
and caches it for faster access.
|
and caches it for faster access.
|
||||||
|
|
||||||
Note:
|
Note:
|
||||||
|
|
@ -161,6 +227,8 @@ class ProductsModel:
|
||||||
# Cache helpers
|
# Cache helpers
|
||||||
self._product_type_items_cache = NestedCacheItem(
|
self._product_type_items_cache = NestedCacheItem(
|
||||||
levels=1, default_factory=list, lifetime=self.lifetime)
|
levels=1, default_factory=list, lifetime=self.lifetime)
|
||||||
|
self._product_base_type_items_cache = NestedCacheItem(
|
||||||
|
levels=1, default_factory=list, lifetime=self.lifetime)
|
||||||
self._product_items_cache = NestedCacheItem(
|
self._product_items_cache = NestedCacheItem(
|
||||||
levels=2, default_factory=dict, lifetime=self.lifetime)
|
levels=2, default_factory=dict, lifetime=self.lifetime)
|
||||||
self._repre_items_cache = NestedCacheItem(
|
self._repre_items_cache = NestedCacheItem(
|
||||||
|
|
@ -199,6 +267,31 @@ class ProductsModel:
|
||||||
])
|
])
|
||||||
return cache.get_data()
|
return cache.get_data()
|
||||||
|
|
||||||
|
def get_product_base_type_items(
|
||||||
|
self,
|
||||||
|
project_name: Optional[str]) -> list[ProductBaseTypeItem]:
|
||||||
|
"""Product base type items for the project.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
project_name (optional, str): Project name.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[ProductBaseTypeDict]: Product base type items.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not project_name:
|
||||||
|
return []
|
||||||
|
|
||||||
|
cache = self._product_base_type_items_cache[project_name]
|
||||||
|
if not cache.is_valid:
|
||||||
|
product_base_types = ayon_api.get_project_product_base_types(
|
||||||
|
project_name)
|
||||||
|
cache.update_data([
|
||||||
|
product_base_type_item_from_data(product_base_type)
|
||||||
|
for product_base_type in product_base_types
|
||||||
|
])
|
||||||
|
return cache.get_data()
|
||||||
|
|
||||||
def get_product_items(self, project_name, folder_ids, sender):
|
def get_product_items(self, project_name, folder_ids, sender):
|
||||||
"""Product items with versions for project and folder ids.
|
"""Product items with versions for project and folder ids.
|
||||||
|
|
||||||
|
|
@ -449,11 +542,12 @@ class ProductsModel:
|
||||||
|
|
||||||
def _create_product_items(
|
def _create_product_items(
|
||||||
self,
|
self,
|
||||||
project_name,
|
project_name: str,
|
||||||
products,
|
products: Iterable[ProductDict],
|
||||||
versions,
|
versions: Iterable[VersionDict],
|
||||||
folder_items=None,
|
folder_items=None,
|
||||||
product_type_items=None,
|
product_type_items=None,
|
||||||
|
product_base_type_items: Optional[Iterable[ProductBaseTypeItem]] = None
|
||||||
):
|
):
|
||||||
if folder_items is None:
|
if folder_items is None:
|
||||||
folder_items = self._controller.get_folder_items(project_name)
|
folder_items = self._controller.get_folder_items(project_name)
|
||||||
|
|
@ -461,6 +555,11 @@ class ProductsModel:
|
||||||
if product_type_items is None:
|
if product_type_items is None:
|
||||||
product_type_items = self.get_product_type_items(project_name)
|
product_type_items = self.get_product_type_items(project_name)
|
||||||
|
|
||||||
|
if product_base_type_items is None:
|
||||||
|
product_base_type_items = self.get_product_base_type_items(
|
||||||
|
project_name
|
||||||
|
)
|
||||||
|
|
||||||
loaded_product_ids = self._controller.get_loaded_product_ids()
|
loaded_product_ids = self._controller.get_loaded_product_ids()
|
||||||
|
|
||||||
versions_by_product_id = collections.defaultdict(list)
|
versions_by_product_id = collections.defaultdict(list)
|
||||||
|
|
@ -470,7 +569,13 @@ class ProductsModel:
|
||||||
product_type_item.name: product_type_item
|
product_type_item.name: product_type_item
|
||||||
for product_type_item in product_type_items
|
for product_type_item in product_type_items
|
||||||
}
|
}
|
||||||
output = {}
|
|
||||||
|
product_base_type_items_by_name: dict[str, ProductBaseTypeItem] = {
|
||||||
|
product_base_type_item.name: product_base_type_item
|
||||||
|
for product_base_type_item in product_base_type_items
|
||||||
|
}
|
||||||
|
|
||||||
|
output: dict[str, ProductItem] = {}
|
||||||
for product in products:
|
for product in products:
|
||||||
product_id = product["id"]
|
product_id = product["id"]
|
||||||
folder_id = product["folderId"]
|
folder_id = product["folderId"]
|
||||||
|
|
@ -484,6 +589,7 @@ class ProductsModel:
|
||||||
product,
|
product,
|
||||||
versions,
|
versions,
|
||||||
product_type_items_by_name,
|
product_type_items_by_name,
|
||||||
|
product_base_type_items_by_name,
|
||||||
folder_item.label,
|
folder_item.label,
|
||||||
product_id in loaded_product_ids,
|
product_id in loaded_product_ids,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -16,31 +16,32 @@ TASK_ID_ROLE = QtCore.Qt.UserRole + 5
|
||||||
PRODUCT_ID_ROLE = QtCore.Qt.UserRole + 6
|
PRODUCT_ID_ROLE = QtCore.Qt.UserRole + 6
|
||||||
PRODUCT_NAME_ROLE = QtCore.Qt.UserRole + 7
|
PRODUCT_NAME_ROLE = QtCore.Qt.UserRole + 7
|
||||||
PRODUCT_TYPE_ROLE = QtCore.Qt.UserRole + 8
|
PRODUCT_TYPE_ROLE = QtCore.Qt.UserRole + 8
|
||||||
PRODUCT_TYPE_ICON_ROLE = QtCore.Qt.UserRole + 9
|
PRODUCT_BASE_TYPE_ROLE = QtCore.Qt.UserRole + 9
|
||||||
PRODUCT_IN_SCENE_ROLE = QtCore.Qt.UserRole + 10
|
PRODUCT_TYPE_ICON_ROLE = QtCore.Qt.UserRole + 10
|
||||||
VERSION_ID_ROLE = QtCore.Qt.UserRole + 11
|
PRODUCT_IN_SCENE_ROLE = QtCore.Qt.UserRole + 11
|
||||||
VERSION_HERO_ROLE = QtCore.Qt.UserRole + 12
|
VERSION_ID_ROLE = QtCore.Qt.UserRole + 12
|
||||||
VERSION_NAME_ROLE = QtCore.Qt.UserRole + 13
|
VERSION_HERO_ROLE = QtCore.Qt.UserRole + 13
|
||||||
VERSION_NAME_EDIT_ROLE = QtCore.Qt.UserRole + 14
|
VERSION_NAME_ROLE = QtCore.Qt.UserRole + 14
|
||||||
VERSION_PUBLISH_TIME_ROLE = QtCore.Qt.UserRole + 15
|
VERSION_NAME_EDIT_ROLE = QtCore.Qt.UserRole + 15
|
||||||
VERSION_STATUS_NAME_ROLE = QtCore.Qt.UserRole + 16
|
VERSION_PUBLISH_TIME_ROLE = QtCore.Qt.UserRole + 16
|
||||||
VERSION_STATUS_SHORT_ROLE = QtCore.Qt.UserRole + 17
|
VERSION_STATUS_NAME_ROLE = QtCore.Qt.UserRole + 17
|
||||||
VERSION_STATUS_COLOR_ROLE = QtCore.Qt.UserRole + 18
|
VERSION_STATUS_SHORT_ROLE = QtCore.Qt.UserRole + 18
|
||||||
VERSION_STATUS_ICON_ROLE = QtCore.Qt.UserRole + 19
|
VERSION_STATUS_COLOR_ROLE = QtCore.Qt.UserRole + 19
|
||||||
VERSION_AUTHOR_ROLE = QtCore.Qt.UserRole + 20
|
VERSION_STATUS_ICON_ROLE = QtCore.Qt.UserRole + 20
|
||||||
VERSION_FRAME_RANGE_ROLE = QtCore.Qt.UserRole + 21
|
VERSION_AUTHOR_ROLE = QtCore.Qt.UserRole + 21
|
||||||
VERSION_DURATION_ROLE = QtCore.Qt.UserRole + 22
|
VERSION_FRAME_RANGE_ROLE = QtCore.Qt.UserRole + 22
|
||||||
VERSION_HANDLES_ROLE = QtCore.Qt.UserRole + 23
|
VERSION_DURATION_ROLE = QtCore.Qt.UserRole + 23
|
||||||
VERSION_STEP_ROLE = QtCore.Qt.UserRole + 24
|
VERSION_HANDLES_ROLE = QtCore.Qt.UserRole + 24
|
||||||
VERSION_AVAILABLE_ROLE = QtCore.Qt.UserRole + 25
|
VERSION_STEP_ROLE = QtCore.Qt.UserRole + 25
|
||||||
VERSION_THUMBNAIL_ID_ROLE = QtCore.Qt.UserRole + 26
|
VERSION_AVAILABLE_ROLE = QtCore.Qt.UserRole + 26
|
||||||
ACTIVE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 27
|
VERSION_THUMBNAIL_ID_ROLE = QtCore.Qt.UserRole + 27
|
||||||
REMOTE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 28
|
ACTIVE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 28
|
||||||
REPRESENTATIONS_COUNT_ROLE = QtCore.Qt.UserRole + 29
|
REMOTE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 29
|
||||||
SYNC_ACTIVE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 30
|
REPRESENTATIONS_COUNT_ROLE = QtCore.Qt.UserRole + 30
|
||||||
SYNC_REMOTE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 31
|
SYNC_ACTIVE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 31
|
||||||
|
SYNC_REMOTE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 32
|
||||||
|
|
||||||
STATUS_NAME_FILTER_ROLE = QtCore.Qt.UserRole + 32
|
STATUS_NAME_FILTER_ROLE = QtCore.Qt.UserRole + 33
|
||||||
|
|
||||||
|
|
||||||
class ProductsModel(QtGui.QStandardItemModel):
|
class ProductsModel(QtGui.QStandardItemModel):
|
||||||
|
|
@ -49,6 +50,7 @@ class ProductsModel(QtGui.QStandardItemModel):
|
||||||
column_labels = [
|
column_labels = [
|
||||||
"Product name",
|
"Product name",
|
||||||
"Product type",
|
"Product type",
|
||||||
|
"Product base type",
|
||||||
"Folder",
|
"Folder",
|
||||||
"Version",
|
"Version",
|
||||||
"Status",
|
"Status",
|
||||||
|
|
@ -79,6 +81,7 @@ class ProductsModel(QtGui.QStandardItemModel):
|
||||||
|
|
||||||
product_name_col = column_labels.index("Product name")
|
product_name_col = column_labels.index("Product name")
|
||||||
product_type_col = column_labels.index("Product type")
|
product_type_col = column_labels.index("Product type")
|
||||||
|
product_base_type_col = column_labels.index("Product base type")
|
||||||
folders_label_col = column_labels.index("Folder")
|
folders_label_col = column_labels.index("Folder")
|
||||||
version_col = column_labels.index("Version")
|
version_col = column_labels.index("Version")
|
||||||
status_col = column_labels.index("Status")
|
status_col = column_labels.index("Status")
|
||||||
|
|
@ -93,6 +96,7 @@ class ProductsModel(QtGui.QStandardItemModel):
|
||||||
_display_role_mapping = {
|
_display_role_mapping = {
|
||||||
product_name_col: QtCore.Qt.DisplayRole,
|
product_name_col: QtCore.Qt.DisplayRole,
|
||||||
product_type_col: PRODUCT_TYPE_ROLE,
|
product_type_col: PRODUCT_TYPE_ROLE,
|
||||||
|
product_base_type_col: PRODUCT_BASE_TYPE_ROLE,
|
||||||
folders_label_col: FOLDER_LABEL_ROLE,
|
folders_label_col: FOLDER_LABEL_ROLE,
|
||||||
version_col: VERSION_NAME_ROLE,
|
version_col: VERSION_NAME_ROLE,
|
||||||
status_col: VERSION_STATUS_NAME_ROLE,
|
status_col: VERSION_STATUS_NAME_ROLE,
|
||||||
|
|
@ -432,6 +436,9 @@ class ProductsModel(QtGui.QStandardItemModel):
|
||||||
model_item.setData(icon, QtCore.Qt.DecorationRole)
|
model_item.setData(icon, QtCore.Qt.DecorationRole)
|
||||||
model_item.setData(product_id, PRODUCT_ID_ROLE)
|
model_item.setData(product_id, PRODUCT_ID_ROLE)
|
||||||
model_item.setData(product_item.product_name, PRODUCT_NAME_ROLE)
|
model_item.setData(product_item.product_name, PRODUCT_NAME_ROLE)
|
||||||
|
model_item.setData(
|
||||||
|
product_item.product_base_type, PRODUCT_BASE_TYPE_ROLE
|
||||||
|
)
|
||||||
model_item.setData(product_item.product_type, PRODUCT_TYPE_ROLE)
|
model_item.setData(product_item.product_type, PRODUCT_TYPE_ROLE)
|
||||||
model_item.setData(product_type_icon, PRODUCT_TYPE_ICON_ROLE)
|
model_item.setData(product_type_icon, PRODUCT_TYPE_ICON_ROLE)
|
||||||
model_item.setData(product_item.folder_id, FOLDER_ID_ROLE)
|
model_item.setData(product_item.folder_id, FOLDER_ID_ROLE)
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ from typing import Optional
|
||||||
|
|
||||||
from qtpy import QtWidgets, QtCore
|
from qtpy import QtWidgets, QtCore
|
||||||
|
|
||||||
|
from ayon_core.pipeline.compatibility import is_product_base_type_supported
|
||||||
from ayon_core.tools.utils import (
|
from ayon_core.tools.utils import (
|
||||||
RecursiveSortFilterProxyModel,
|
RecursiveSortFilterProxyModel,
|
||||||
DeselectableTreeView,
|
DeselectableTreeView,
|
||||||
|
|
@ -142,6 +143,7 @@ class ProductsWidget(QtWidgets.QWidget):
|
||||||
default_widths = (
|
default_widths = (
|
||||||
200, # Product name
|
200, # Product name
|
||||||
90, # Product type
|
90, # Product type
|
||||||
|
90, # Product base type
|
||||||
130, # Folder label
|
130, # Folder label
|
||||||
60, # Version
|
60, # Version
|
||||||
100, # Status
|
100, # Status
|
||||||
|
|
@ -261,6 +263,12 @@ class ProductsWidget(QtWidgets.QWidget):
|
||||||
self._controller.is_sitesync_enabled()
|
self._controller.is_sitesync_enabled()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not is_product_base_type_supported():
|
||||||
|
# Hide product base type column
|
||||||
|
products_view.setColumnHidden(
|
||||||
|
products_model.product_base_type_col, True
|
||||||
|
)
|
||||||
|
|
||||||
def set_name_filter(self, name):
|
def set_name_filter(self, name):
|
||||||
"""Set filter of product name.
|
"""Set filter of product name.
|
||||||
|
|
||||||
|
|
|
||||||
88
tests/client/ayon_core/pipeline/load/test_loaders.py
Normal file
88
tests/client/ayon_core/pipeline/load/test_loaders.py
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
"""Test loaders in the pipeline module."""
|
||||||
|
|
||||||
|
from ayon_core.pipeline.load import LoaderPlugin
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_compatible_loader():
|
||||||
|
"""Test if a loader is compatible with a given representation."""
|
||||||
|
from ayon_core.pipeline.load import is_compatible_loader
|
||||||
|
|
||||||
|
# Create a mock representation context
|
||||||
|
context = {
|
||||||
|
"loader": "test_loader",
|
||||||
|
"representation": {"name": "test_representation"},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create a mock loader plugin
|
||||||
|
class MockLoader(LoaderPlugin):
|
||||||
|
name = "test_loader"
|
||||||
|
version = "1.0.0"
|
||||||
|
|
||||||
|
def is_compatible_loader(self, context):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Check compatibility
|
||||||
|
assert is_compatible_loader(MockLoader(), context) is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_complex_is_compatible_loader():
|
||||||
|
"""Test if a loader is compatible with a complex representation."""
|
||||||
|
from ayon_core.pipeline.load import is_compatible_loader
|
||||||
|
|
||||||
|
# Create a mock complex representation context
|
||||||
|
context = {
|
||||||
|
"loader": "complex_loader",
|
||||||
|
"representation": {
|
||||||
|
"name": "complex_representation",
|
||||||
|
"extension": "exr"
|
||||||
|
},
|
||||||
|
"additional_data": {"key": "value"},
|
||||||
|
"product": {
|
||||||
|
"name": "complex_product",
|
||||||
|
"productType": "foo",
|
||||||
|
"productBaseType": "bar",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create a mock loader plugin
|
||||||
|
class ComplexLoaderA(LoaderPlugin):
|
||||||
|
name = "complex_loaderA"
|
||||||
|
|
||||||
|
# False because the loader doesn't specify any compatibility (missing
|
||||||
|
# wildcard for product type and product base type)
|
||||||
|
assert is_compatible_loader(ComplexLoaderA(), context) is False
|
||||||
|
|
||||||
|
class ComplexLoaderB(LoaderPlugin):
|
||||||
|
name = "complex_loaderB"
|
||||||
|
product_types = {"*"}
|
||||||
|
representations = {"*"}
|
||||||
|
|
||||||
|
# True, it is compatible with any product type
|
||||||
|
assert is_compatible_loader(ComplexLoaderB(), context) is True
|
||||||
|
|
||||||
|
class ComplexLoaderC(LoaderPlugin):
|
||||||
|
name = "complex_loaderC"
|
||||||
|
product_base_types = {"*"}
|
||||||
|
representations = {"*"}
|
||||||
|
|
||||||
|
# True, it is compatible with any product base type
|
||||||
|
assert is_compatible_loader(ComplexLoaderC(), context) is True
|
||||||
|
|
||||||
|
class ComplexLoaderD(LoaderPlugin):
|
||||||
|
name = "complex_loaderD"
|
||||||
|
product_types = {"foo"}
|
||||||
|
representations = {"*"}
|
||||||
|
|
||||||
|
# legacy loader defining compatibility only with product type
|
||||||
|
# is compatible provided the same product type is defined in context
|
||||||
|
assert is_compatible_loader(ComplexLoaderD(), context) is False
|
||||||
|
|
||||||
|
class ComplexLoaderE(LoaderPlugin):
|
||||||
|
name = "complex_loaderE"
|
||||||
|
product_types = {"foo"}
|
||||||
|
representations = {"*"}
|
||||||
|
|
||||||
|
# remove productBaseType from context to simulate legacy behavior
|
||||||
|
context["product"].pop("productBaseType", None)
|
||||||
|
|
||||||
|
assert is_compatible_loader(ComplexLoaderE(), context) is True
|
||||||
Loading…
Add table
Add a link
Reference in a new issue