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:
Jakub Trllo 2025-06-24 11:49:06 +02:00 committed by GitHub
commit b2ab8ef5c2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 386 additions and 101 deletions

View file

@ -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:

View file

@ -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)

View file

@ -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,
) )

View file

@ -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)

View file

@ -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.

View 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