Merge branch 'develop' into enhancing-publisher-card-readability

This commit is contained in:
Jakub Trllo 2025-11-10 11:09:10 +01:00 committed by GitHub
commit 6d573b6c70
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 278 additions and 80 deletions

View file

@ -1,8 +1,15 @@
from __future__ import annotations
import os
from abc import ABC, abstractmethod
import typing
from typing import Optional
from ayon_core.style import get_default_entity_icon_color
if typing.TYPE_CHECKING:
from ayon_core.host import PublishedWorkfileInfo
class FolderItem:
"""Item representing folder entity on a server.
@ -159,6 +166,17 @@ class WorkareaFilepathResult:
self.filepath = filepath
class PublishedWorkfileWrap:
"""Wrapper for workfile info that also contains version comment."""
def __init__(
self,
info: Optional[PublishedWorkfileInfo] = None,
comment: Optional[str] = None,
) -> None:
self.info = info
self.comment = comment
class AbstractWorkfilesCommon(ABC):
@abstractmethod
def is_host_valid(self):
@ -787,6 +805,25 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon):
"""
pass
@abstractmethod
def get_published_workfile_info(
self,
folder_id: Optional[str],
representation_id: Optional[str],
) -> PublishedWorkfileWrap:
"""Get published workfile info by representation ID.
Args:
folder_id (Optional[str]): Folder id.
representation_id (Optional[str]): Representation id.
Returns:
PublishedWorkfileWrap: Published workfile info or None
if not found.
"""
pass
@abstractmethod
def get_workfile_info(self, folder_id, task_id, rootless_path):
"""Workfile info from database.

View file

@ -1,4 +1,7 @@
from __future__ import annotations
import os
from typing import Optional
import ayon_api
@ -18,6 +21,7 @@ from ayon_core.tools.common_models import (
from .abstract import (
AbstractWorkfilesBackend,
AbstractWorkfilesFrontend,
PublishedWorkfileWrap,
)
from .models import SelectionModel, WorkfilesModel
@ -432,6 +436,15 @@ class BaseWorkfileController(
folder_id, task_id
)
def get_published_workfile_info(
self,
folder_id: Optional[str],
representation_id: Optional[str],
) -> PublishedWorkfileWrap:
return self._workfiles_model.get_published_workfile_info(
folder_id, representation_id
)
def get_workfile_info(self, folder_id, task_id, rootless_path):
return self._workfiles_model.get_workfile_info(
folder_id, task_id, rootless_path

View file

@ -17,6 +17,8 @@ class SelectionModel(object):
self._task_name = None
self._task_id = None
self._workfile_path = None
self._rootless_workfile_path = None
self._workfile_entity_id = None
self._representation_id = None
def get_selected_folder_id(self):
@ -62,39 +64,49 @@ class SelectionModel(object):
def get_selected_workfile_path(self):
return self._workfile_path
def get_selected_workfile_data(self):
return {
"project_name": self._controller.get_current_project_name(),
"path": self._workfile_path,
"rootless_path": self._rootless_workfile_path,
"folder_id": self._folder_id,
"task_name": self._task_name,
"task_id": self._task_id,
"workfile_entity_id": self._workfile_entity_id,
}
def set_selected_workfile_path(
self, rootless_path, path, workfile_entity_id
):
if path == self._workfile_path:
return
self._rootless_workfile_path = rootless_path
self._workfile_path = path
self._workfile_entity_id = workfile_entity_id
self._controller.emit_event(
"selection.workarea.changed",
{
"project_name": self._controller.get_current_project_name(),
"path": path,
"rootless_path": rootless_path,
"folder_id": self._folder_id,
"task_name": self._task_name,
"task_id": self._task_id,
"workfile_entity_id": workfile_entity_id,
},
self.get_selected_workfile_data(),
self.event_source
)
def get_selected_representation_id(self):
return self._representation_id
def get_selected_representation_data(self):
return {
"project_name": self._controller.get_current_project_name(),
"folder_id": self._folder_id,
"task_id": self._task_id,
"representation_id": self._representation_id,
}
def set_selected_representation_id(self, representation_id):
if representation_id == self._representation_id:
return
self._representation_id = representation_id
self._controller.emit_event(
"selection.representation.changed",
{
"project_name": self._controller.get_current_project_name(),
"representation_id": representation_id,
},
self.get_selected_representation_data(),
self.event_source
)

View file

@ -39,6 +39,7 @@ from ayon_core.pipeline.workfile import (
from ayon_core.pipeline.version_start import get_versioning_start
from ayon_core.tools.workfiles.abstract import (
WorkareaFilepathResult,
PublishedWorkfileWrap,
AbstractWorkfilesBackend,
)
@ -79,6 +80,7 @@ class WorkfilesModel:
# Published workfiles
self._repre_by_id = {}
self._version_comment_by_id = {}
self._published_workfile_items_cache = NestedCacheItem(
levels=1, default_factory=list
)
@ -95,6 +97,7 @@ class WorkfilesModel:
self._workarea_file_items_cache.reset()
self._repre_by_id = {}
self._version_comment_by_id = {}
self._published_workfile_items_cache.reset()
self._workfile_entities_by_task_id = {}
@ -552,13 +555,13 @@ class WorkfilesModel:
)
def get_published_file_items(
self, folder_id: str, task_id: str
self, folder_id: Optional[str], task_id: Optional[str]
) -> list[PublishedWorkfileInfo]:
"""Published workfiles for passed context.
Args:
folder_id (str): Folder id.
task_id (str): Task id.
folder_id (Optional[str]): Folder id.
task_id (Optional[str]): Task id.
Returns:
list[PublishedWorkfileInfo]: List of files for published workfiles.
@ -586,7 +589,7 @@ class WorkfilesModel:
version_entities = list(ayon_api.get_versions(
project_name,
product_ids=product_ids,
fields={"id", "author", "taskId"},
fields={"id", "author", "taskId", "attrib.comment"},
))
repre_entities = []
@ -600,6 +603,13 @@ class WorkfilesModel:
repre_entity["id"]: repre_entity
for repre_entity in repre_entities
})
# Map versions by representation ID for easy lookup
self._version_comment_by_id.update({
version_entity["id"]: version_entity["attrib"].get("comment")
for version_entity in version_entities
})
project_entity = self._controller.get_project_entity(project_name)
prepared_data = ListPublishedWorkfilesOptionalData(
@ -626,6 +636,34 @@ class WorkfilesModel:
]
return items
def get_published_workfile_info(
self,
folder_id: Optional[str],
representation_id: Optional[str],
) -> PublishedWorkfileWrap:
"""Get published workfile info by representation ID.
Args:
folder_id (Optional[str]): Folder id.
representation_id (Optional[str]): Representation id.
Returns:
PublishedWorkfileWrap: Published workfile info or None
if not found.
"""
if not representation_id:
return PublishedWorkfileWrap()
# Search through all cached published workfile items
for item in self.get_published_file_items(folder_id, None):
if item.representation_id == representation_id:
comment = self._get_published_workfile_version_comment(
representation_id
)
return PublishedWorkfileWrap(item, comment)
return PublishedWorkfileWrap()
@property
def _project_name(self) -> str:
return self._controller.get_current_project_name()
@ -642,6 +680,25 @@ class WorkfilesModel:
self._current_username = get_ayon_username()
return self._current_username
def _get_published_workfile_version_comment(
self, representation_id: str
) -> Optional[str]:
"""Get version comment for published workfile.
Args:
representation_id (str): Representation id.
Returns:
Optional[str]: Version comment or None.
"""
if not representation_id:
return None
repre = self._repre_by_id.get(representation_id)
if not repre:
return None
return self._version_comment_by_id.get(repre["versionId"])
# --- Host ---
def _open_workfile(self, folder_id: str, task_id: str, filepath: str):
# TODO move to workfiles pipeline

View file

@ -1,6 +1,7 @@
import datetime
from typing import Optional
from qtpy import QtWidgets, QtCore
from qtpy import QtCore, QtWidgets
def file_size_to_string(file_size):
@ -8,9 +9,9 @@ def file_size_to_string(file_size):
return "N/A"
size = 0
size_ending_mapping = {
"KB": 1024 ** 1,
"MB": 1024 ** 2,
"GB": 1024 ** 3
"KB": 1024**1,
"MB": 1024**2,
"GB": 1024**3,
}
ending = "B"
for _ending, _size in size_ending_mapping.items():
@ -70,7 +71,12 @@ class SidePanelWidget(QtWidgets.QWidget):
btn_description_save.clicked.connect(self._on_save_click)
controller.register_event_callback(
"selection.workarea.changed", self._on_selection_change
"selection.workarea.changed",
self._on_workarea_selection_change
)
controller.register_event_callback(
"selection.representation.changed",
self._on_representation_selection_change,
)
self._details_input = details_input
@ -82,12 +88,13 @@ class SidePanelWidget(QtWidgets.QWidget):
self._task_id = None
self._filepath = None
self._rootless_path = None
self._representation_id = None
self._orig_description = ""
self._controller = controller
self._set_context(None, None, None, None)
self._set_context(False, None, None)
def set_published_mode(self, published_mode):
def set_published_mode(self, published_mode: bool) -> None:
"""Change published mode.
Args:
@ -95,14 +102,37 @@ class SidePanelWidget(QtWidgets.QWidget):
"""
self._description_widget.setVisible(not published_mode)
# Clear the context when switching modes to avoid showing stale data
if published_mode:
self._set_publish_context(
self._folder_id,
self._task_id,
self._representation_id,
)
else:
self._set_workarea_context(
self._folder_id,
self._task_id,
self._rootless_path,
self._filepath,
)
def _on_selection_change(self, event):
def _on_workarea_selection_change(self, event):
folder_id = event["folder_id"]
task_id = event["task_id"]
filepath = event["path"]
rootless_path = event["rootless_path"]
self._set_context(folder_id, task_id, rootless_path, filepath)
self._set_workarea_context(
folder_id, task_id, rootless_path, filepath
)
def _on_representation_selection_change(self, event):
folder_id = event["folder_id"]
task_id = event["task_id"]
representation_id = event["representation_id"]
self._set_publish_context(folder_id, task_id, representation_id)
def _on_description_change(self):
text = self._description_input.toPlainText()
@ -118,85 +148,134 @@ class SidePanelWidget(QtWidgets.QWidget):
self._orig_description = description
self._btn_description_save.setEnabled(False)
def _set_context(self, folder_id, task_id, rootless_path, filepath):
def _set_workarea_context(
self,
folder_id: Optional[str],
task_id: Optional[str],
rootless_path: Optional[str],
filepath: Optional[str],
) -> None:
self._rootless_path = rootless_path
self._filepath = filepath
workfile_info = None
# Check if folder, task and file are selected
if folder_id and task_id and rootless_path:
workfile_info = self._controller.get_workfile_info(
folder_id, task_id, rootless_path
)
enabled = workfile_info is not None
self._details_input.setEnabled(enabled)
self._description_input.setEnabled(enabled)
self._btn_description_save.setEnabled(enabled)
self._folder_id = folder_id
self._task_id = task_id
self._filepath = filepath
self._rootless_path = rootless_path
# Disable inputs and remove texts if any required arguments are
# missing
if not enabled:
if workfile_info is None:
self._orig_description = ""
self._details_input.setPlainText("")
self._description_input.setPlainText("")
self._set_context(False, folder_id, task_id)
return
description = workfile_info.description
size_value = file_size_to_string(workfile_info.file_size)
self._set_context(
True,
folder_id,
task_id,
file_created=workfile_info.file_created,
file_modified=workfile_info.file_modified,
size_value=workfile_info.file_size,
created_by=workfile_info.created_by,
updated_by=workfile_info.updated_by,
)
description = workfile_info.description
self._orig_description = description
self._description_input.setPlainText(description)
def _set_publish_context(
self,
folder_id: Optional[str],
task_id: Optional[str],
representation_id: Optional[str],
) -> None:
self._representation_id = representation_id
published_workfile_wrap = self._controller.get_published_workfile_info(
folder_id,
representation_id,
)
info = published_workfile_wrap.info
comment = published_workfile_wrap.comment
if info is None:
self._set_context(False, folder_id, task_id)
return
self._set_context(
True,
folder_id,
task_id,
file_created=info.file_created,
file_modified=info.file_modified,
size_value=info.file_size,
created_by=info.author,
comment=comment,
)
def _set_context(
self,
is_valid: bool,
folder_id: Optional[str],
task_id: Optional[str],
*,
file_created: Optional[int] = None,
file_modified: Optional[int] = None,
size_value: Optional[int] = None,
created_by: Optional[str] = None,
updated_by: Optional[str] = None,
comment: Optional[str] = None,
) -> None:
self._folder_id = folder_id
self._task_id = task_id
self._details_input.setEnabled(is_valid)
self._description_input.setEnabled(is_valid)
self._btn_description_save.setEnabled(is_valid)
if not is_valid:
self._details_input.setPlainText("")
return
# Append html string
datetime_format = "%b %d %Y %H:%M:%S"
file_created = workfile_info.file_created
modification_time = workfile_info.file_modified
if file_created:
file_created = datetime.datetime.fromtimestamp(file_created)
if modification_time:
modification_time = datetime.datetime.fromtimestamp(
modification_time)
if file_modified:
file_modified = datetime.datetime.fromtimestamp(
file_modified
)
user_items_by_name = self._controller.get_user_items_by_name()
def convert_username(username):
user_item = user_items_by_name.get(username)
def convert_username(username_v):
user_item = user_items_by_name.get(username_v)
if user_item is not None and user_item.full_name:
return user_item.full_name
return username
return username_v
created_lines = []
if workfile_info.created_by:
created_lines.append(
convert_username(workfile_info.created_by)
)
if file_created:
created_lines.append(file_created.strftime(datetime_format))
lines = []
if size_value is not None:
size_value = file_size_to_string(size_value)
lines.append(f"<b>Size:</b><br/>{size_value}")
if created_lines:
created_lines.insert(0, "<b>Created:</b>")
# Add version comment for published workfiles
if comment:
lines.append(f"<b>Comment:</b><br/>{comment}")
modified_lines = []
if workfile_info.updated_by:
modified_lines.append(
convert_username(workfile_info.updated_by)
)
if modification_time:
modified_lines.append(
modification_time.strftime(datetime_format)
)
if modified_lines:
modified_lines.insert(0, "<b>Modified:</b>")
if created_by or file_created:
lines.append("<b>Created:</b>")
if created_by:
lines.append(convert_username(created_by))
if file_created:
lines.append(file_created.strftime(datetime_format))
lines = (
"<b>Size:</b>",
size_value,
"<br/>".join(created_lines),
"<br/>".join(modified_lines),
)
self._orig_description = description
self._description_input.setPlainText(description)
if updated_by or file_modified:
lines.append("<b>Modified:</b>")
if updated_by:
lines.append(convert_username(updated_by))
if file_modified:
lines.append(file_modified.strftime(datetime_format))
# Set as empty string
self._details_input.setPlainText("")