Merge pull request #548 from ynput/feature/AY-5566_Add-statuses-to-loader

Loader: Show statuses in loader
This commit is contained in:
Jakub Trllo 2024-05-24 15:57:34 +02:00 committed by GitHub
commit 55d8f11ec3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 291 additions and 59 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -6,6 +6,9 @@ from ayon_core.tools.utils.lib import format_version
from .products_model import (
PRODUCT_ID_ROLE,
VERSION_NAME_EDIT_ROLE,
VERSION_STATUS_NAME_ROLE,
VERSION_STATUS_SHORT_ROLE,
VERSION_STATUS_COLOR_ROLE,
VERSION_ID_ROLE,
PRODUCT_IN_SCENE_ROLE,
ACTIVE_SITE_ICON_ROLE,
@ -194,6 +197,49 @@ class LoadedInSceneDelegate(QtWidgets.QStyledItemDelegate):
option.palette.setBrush(QtGui.QPalette.Text, color)
class StatusDelegate(QtWidgets.QStyledItemDelegate):
"""Delegate showing status name and short name."""
def paint(self, painter, option, index):
if option.widget:
style = option.widget.style()
else:
style = QtWidgets.QApplication.style()
style.drawControl(
style.CE_ItemViewItem, option, painter, option.widget
)
painter.save()
text_rect = style.subElementRect(style.SE_ItemViewItemText, option)
text_margin = style.proxy().pixelMetric(
style.PM_FocusFrameHMargin, option, option.widget
) + 1
padded_text_rect = text_rect.adjusted(
text_margin, 0, - text_margin, 0
)
fm = QtGui.QFontMetrics(option.font)
text = index.data(VERSION_STATUS_NAME_ROLE)
if padded_text_rect.width() < fm.width(text):
text = index.data(VERSION_STATUS_SHORT_ROLE)
status_color = index.data(VERSION_STATUS_COLOR_ROLE)
fg_color = QtGui.QColor(status_color)
pen = painter.pen()
pen.setColor(fg_color)
painter.setPen(pen)
painter.drawText(
padded_text_rect,
option.displayAlignment,
text
)
painter.restore()
class SiteSyncDelegate(QtWidgets.QStyledItemDelegate):
"""Paints icons and downloaded representation ration for both sites."""

View file

@ -22,18 +22,21 @@ 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_AUTHOR_ROLE = QtCore.Qt.UserRole + 18
VERSION_FRAME_RANGE_ROLE = QtCore.Qt.UserRole + 19
VERSION_DURATION_ROLE = QtCore.Qt.UserRole + 20
VERSION_HANDLES_ROLE = QtCore.Qt.UserRole + 21
VERSION_STEP_ROLE = QtCore.Qt.UserRole + 22
VERSION_AVAILABLE_ROLE = QtCore.Qt.UserRole + 23
VERSION_THUMBNAIL_ID_ROLE = QtCore.Qt.UserRole + 24
ACTIVE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 25
REMOTE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 26
REPRESENTATIONS_COUNT_ROLE = QtCore.Qt.UserRole + 27
SYNC_ACTIVE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 28
SYNC_REMOTE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 29
class ProductsModel(QtGui.QStandardItemModel):
@ -44,6 +47,7 @@ class ProductsModel(QtGui.QStandardItemModel):
"Product type",
"Folder",
"Version",
"Status",
"Time",
"Author",
"Frames",
@ -69,11 +73,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 +124,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 +170,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 +206,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 +329,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 +411,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

View file

@ -22,7 +22,8 @@ from .products_model import (
from .products_delegates import (
VersionDelegate,
LoadedInSceneDelegate,
SiteSyncDelegate
StatusDelegate,
SiteSyncDelegate,
)
from .actions_utils import show_actions_menu
@ -89,6 +90,7 @@ class ProductsWidget(QtWidgets.QWidget):
90, # Product type
130, # Folder label
60, # Version
100, # Status
125, # Time
75, # Author
75, # Frames
@ -128,20 +130,19 @@ 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()
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 +176,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