diff --git a/client/ayon_core/plugins/publish/validate_version.py b/client/ayon_core/plugins/publish/validate_version.py
index 25a5757330..c2f7d5bf44 100644
--- a/client/ayon_core/plugins/publish/validate_version.py
+++ b/client/ayon_core/plugins/publish/validate_version.py
@@ -1,6 +1,10 @@
import pyblish.api
+
+from ayon_core.lib import filter_profiles
from ayon_core.pipeline.publish import (
- PublishValidationError, OptionalPyblishPluginMixin
+ PublishValidationError,
+ OptionalPyblishPluginMixin,
+ get_current_host_name,
)
@@ -13,12 +17,35 @@ class ValidateVersion(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin):
order = pyblish.api.ValidatorOrder
label = "Validate Version"
- hosts = ["nuke", "maya", "houdini", "blender",
- "photoshop", "aftereffects"]
optional = False
active = True
+ @classmethod
+ def apply_settings(cls, settings):
+ # Disable if no profile is found for the current host
+ profiles = (
+ settings
+ ["core"]
+ ["publish"]
+ ["ValidateVersion"]
+ ["plugin_state_profiles"]
+ )
+ profile = filter_profiles(
+ profiles, {"host_names": get_current_host_name()}
+ )
+ if not profile:
+ cls.enabled = False
+ return
+
+ # Apply settings from profile
+ for attr_name in {
+ "enabled",
+ "optional",
+ "active",
+ }:
+ setattr(cls, attr_name, profile[attr_name])
+
def process(self, instance):
if not self.is_active(instance.data):
return
diff --git a/client/ayon_core/tools/common_models/__init__.py b/client/ayon_core/tools/common_models/__init__.py
index 8895515b1a..f09edfeab2 100644
--- a/client/ayon_core/tools/common_models/__init__.py
+++ b/client/ayon_core/tools/common_models/__init__.py
@@ -14,6 +14,7 @@ from .hierarchy import (
)
from .thumbnails import ThumbnailsModel
from .selection import HierarchyExpectedSelection
+from .users import UsersModel
__all__ = (
@@ -32,4 +33,6 @@ __all__ = (
"ThumbnailsModel",
"HierarchyExpectedSelection",
+
+ "UsersModel",
)
diff --git a/client/ayon_core/tools/common_models/users.py b/client/ayon_core/tools/common_models/users.py
new file mode 100644
index 0000000000..f8beb31aa1
--- /dev/null
+++ b/client/ayon_core/tools/common_models/users.py
@@ -0,0 +1,84 @@
+import ayon_api
+
+from ayon_core.lib import CacheItem
+
+
+class UserItem:
+ def __init__(
+ self,
+ username,
+ full_name,
+ email,
+ avatar_url,
+ active,
+ ):
+ self.username = username
+ self.full_name = full_name
+ self.email = email
+ self.avatar_url = avatar_url
+ self.active = active
+
+ @classmethod
+ def from_entity_data(cls, user_data):
+ return cls(
+ user_data["name"],
+ user_data["attrib"]["fullName"],
+ user_data["attrib"]["email"],
+ user_data["attrib"]["avatarUrl"],
+ user_data["active"],
+ )
+
+
+class UsersModel:
+ def __init__(self, controller):
+ self._controller = controller
+ self._users_cache = CacheItem(default_factory=list)
+
+ def get_user_items(self):
+ """Get user items.
+
+ Returns:
+ List[UserItem]: List of user items.
+
+ """
+ self._invalidate_cache()
+ return self._users_cache.get_data()
+
+ def get_user_items_by_name(self):
+ """Get user items by name.
+
+ Implemented as most of cases using this model will need to find
+ user information by username.
+
+ Returns:
+ Dict[str, UserItem]: Dictionary of user items by name.
+
+ """
+ return {
+ user_item.username: user_item
+ for user_item in self.get_user_items()
+ }
+
+ def get_user_item_by_username(self, username):
+ """Get user item by username.
+
+ Args:
+ username (str): Username.
+
+ Returns:
+ Union[UserItem, None]: User item or None if not found.
+
+ """
+ self._invalidate_cache()
+ for user_item in self.get_user_items():
+ if user_item.username == username:
+ return user_item
+ return None
+
+ def _invalidate_cache(self):
+ if self._users_cache.is_valid:
+ return
+ self._users_cache.update_data([
+ UserItem.from_entity_data(user)
+ for user in ayon_api.get_users()
+ ])
diff --git a/client/ayon_core/tools/workfiles/abstract.py b/client/ayon_core/tools/workfiles/abstract.py
index c9eb9004e3..f345e20dca 100644
--- a/client/ayon_core/tools/workfiles/abstract.py
+++ b/client/ayon_core/tools/workfiles/abstract.py
@@ -13,8 +13,10 @@ class WorkfileInfo:
task_id (str): Task id.
filepath (str): Filepath.
filesize (int): File size.
- creation_time (int): Creation time (timestamp).
- modification_time (int): Modification time (timestamp).
+ creation_time (float): Creation time (timestamp).
+ modification_time (float): Modification time (timestamp).
+ created_by (Union[str, none]): User who created the file.
+ updated_by (Union[str, none]): User who last updated the file.
note (str): Note.
"""
@@ -26,6 +28,8 @@ class WorkfileInfo:
filesize,
creation_time,
modification_time,
+ created_by,
+ updated_by,
note,
):
self.folder_id = folder_id
@@ -34,6 +38,8 @@ class WorkfileInfo:
self.filesize = filesize
self.creation_time = creation_time
self.modification_time = modification_time
+ self.created_by = created_by
+ self.updated_by = updated_by
self.note = note
def to_data(self):
@@ -50,6 +56,8 @@ class WorkfileInfo:
"filesize": self.filesize,
"creation_time": self.creation_time,
"modification_time": self.modification_time,
+ "created_by": self.created_by,
+ "updated_by": self.updated_by,
"note": self.note,
}
@@ -212,6 +220,7 @@ class FileItem:
dirpath (str): Directory path of file.
filename (str): Filename.
modified (float): Modified timestamp.
+ created_by (Optional[str]): Username.
representation_id (Optional[str]): Representation id of published
workfile.
filepath (Optional[str]): Prepared filepath.
@@ -223,6 +232,8 @@ class FileItem:
dirpath,
filename,
modified,
+ created_by=None,
+ updated_by=None,
representation_id=None,
filepath=None,
exists=None
@@ -230,6 +241,8 @@ class FileItem:
self.filename = filename
self.dirpath = dirpath
self.modified = modified
+ self.created_by = created_by
+ self.updated_by = updated_by
self.representation_id = representation_id
self._filepath = filepath
self._exists = exists
@@ -269,6 +282,7 @@ class FileItem:
"filename": self.filename,
"dirpath": self.dirpath,
"modified": self.modified,
+ "created_by": self.created_by,
"representation_id": self.representation_id,
"filepath": self.filepath,
"exists": self.exists,
@@ -522,6 +536,16 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon):
pass
+ @abstractmethod
+ def get_user_items_by_name(self):
+ """Get user items available on AYON server.
+
+ Returns:
+ Dict[str, UserItem]: User items by username.
+
+ """
+ pass
+
# Host information
@abstractmethod
def get_workfile_extensions(self):
diff --git a/client/ayon_core/tools/workfiles/control.py b/client/ayon_core/tools/workfiles/control.py
index 3048e6be94..8fa9135bc0 100644
--- a/client/ayon_core/tools/workfiles/control.py
+++ b/client/ayon_core/tools/workfiles/control.py
@@ -19,6 +19,7 @@ from ayon_core.tools.common_models import (
HierarchyModel,
HierarchyExpectedSelection,
ProjectsModel,
+ UsersModel,
)
from .abstract import (
@@ -161,6 +162,7 @@ class BaseWorkfileController(
self._save_is_enabled = True
# Expected selected folder and task
+ self._users_model = self._create_users_model()
self._expected_selection = self._create_expected_selection_obj()
self._selection_model = self._create_selection_model()
self._projects_model = self._create_projects_model()
@@ -176,6 +178,12 @@ class BaseWorkfileController(
def is_host_valid(self):
return self._host_is_valid
+ def _create_users_model(self):
+ return UsersModel(self)
+
+ def _create_workfiles_model(self):
+ return WorkfilesModel(self)
+
def _create_expected_selection_obj(self):
return WorkfilesToolExpectedSelection(self)
@@ -188,9 +196,6 @@ class BaseWorkfileController(
def _create_hierarchy_model(self):
return HierarchyModel(self)
- def _create_workfiles_model(self):
- return WorkfilesModel(self)
-
@property
def event_system(self):
"""Inner event system for workfiles tool controller.
@@ -272,6 +277,9 @@ class BaseWorkfileController(
{"enabled": enabled}
)
+ def get_user_items_by_name(self):
+ return self._users_model.get_user_items_by_name()
+
# Host information
def get_workfile_extensions(self):
host = self._host
diff --git a/client/ayon_core/tools/workfiles/models/workfiles.py b/client/ayon_core/tools/workfiles/models/workfiles.py
index 5f59b99b22..c93bbb6637 100644
--- a/client/ayon_core/tools/workfiles/models/workfiles.py
+++ b/client/ayon_core/tools/workfiles/models/workfiles.py
@@ -6,6 +6,7 @@ import arrow
import ayon_api
from ayon_api.operations import OperationsSession
+from ayon_core.lib import get_ayon_username
from ayon_core.pipeline.template_data import (
get_template_data,
get_task_template_data,
@@ -23,6 +24,8 @@ from ayon_core.tools.workfiles.abstract import (
WorkfileInfo,
)
+_NOT_SET = object()
+
class CommentMatcher(object):
"""Use anatomy and work file data to parse comments from filenames.
@@ -188,10 +191,17 @@ class WorkareaModel:
if ext not in self._extensions:
continue
- modified = os.path.getmtime(filepath)
- items.append(
- FileItem(workdir, filename, modified)
+ workfile_info = self._controller.get_workfile_info(
+ folder_id, task_id, filepath
)
+ modified = os.path.getmtime(filepath)
+ items.append(FileItem(
+ workdir,
+ filename,
+ modified,
+ workfile_info.created_by,
+ workfile_info.updated_by,
+ ))
return items
def _get_template_key(self, fill_data):
@@ -439,6 +449,7 @@ class WorkfileEntitiesModel:
self._controller = controller
self._cache = {}
self._items = {}
+ self._current_username = _NOT_SET
def _get_workfile_info_identifier(
self, folder_id, task_id, rootless_path
@@ -459,8 +470,12 @@ class WorkfileEntitiesModel:
self, folder_id, task_id, workfile_info, filepath
):
note = ""
+ created_by = None
+ updated_by = None
if workfile_info:
note = workfile_info["attrib"].get("description") or ""
+ created_by = workfile_info.get("createdBy")
+ updated_by = workfile_info.get("updatedBy")
filestat = os.stat(filepath)
return WorkfileInfo(
@@ -470,6 +485,8 @@ class WorkfileEntitiesModel:
filesize=filestat.st_size,
creation_time=filestat.st_ctime,
modification_time=filestat.st_mtime,
+ created_by=created_by,
+ updated_by=updated_by,
note=note
)
@@ -481,7 +498,7 @@ class WorkfileEntitiesModel:
for workfile_info in ayon_api.get_workfiles_info(
self._controller.get_current_project_name(),
task_ids=[task_id],
- fields=["id", "path", "attrib"],
+ fields=["id", "path", "attrib", "createdBy", "updatedBy"],
):
workfile_identifier = self._get_workfile_info_identifier(
folder_id, task_id, workfile_info["path"]
@@ -525,18 +542,32 @@ class WorkfileEntitiesModel:
self._items.pop(identifier, None)
return
- if note is None:
- return
-
old_note = workfile_info.get("attrib", {}).get("note")
new_workfile_info = copy.deepcopy(workfile_info)
- attrib = new_workfile_info.setdefault("attrib", {})
- attrib["description"] = note
+ update_data = {}
+ if note is not None and old_note != note:
+ update_data["attrib"] = {"description": note}
+ attrib = new_workfile_info.setdefault("attrib", {})
+ attrib["description"] = note
+
+ username = self._get_current_username()
+ # Automatically fix 'createdBy' and 'updatedBy' fields
+ # NOTE both fields were not automatically filled by server
+ # until 1.1.3 release.
+ if workfile_info.get("createdBy") is None:
+ update_data["createdBy"] = username
+ new_workfile_info["createdBy"] = username
+
+ if workfile_info.get("updatedBy") != username:
+ update_data["updatedBy"] = username
+ new_workfile_info["updatedBy"] = username
+
+ if not update_data:
+ return
+
self._cache[identifier] = new_workfile_info
self._items.pop(identifier, None)
- if old_note == note:
- return
project_name = self._controller.get_current_project_name()
@@ -545,7 +576,7 @@ class WorkfileEntitiesModel:
project_name,
"workfile",
workfile_info["id"],
- {"attrib": {"description": note}},
+ update_data,
)
session.commit()
@@ -554,13 +585,18 @@ class WorkfileEntitiesModel:
project_name = self._controller.get_current_project_name()
+ username = self._get_current_username()
workfile_info = {
"path": rootless_path,
"taskId": task_id,
"attrib": {
"extension": extension,
"description": note
- }
+ },
+ # TODO remove 'createdBy' and 'updatedBy' fields when server is
+ # or above 1.1.3 .
+ "createdBy": username,
+ "updatedBy": username,
}
session = OperationsSession()
@@ -568,6 +604,11 @@ class WorkfileEntitiesModel:
session.commit()
return workfile_info
+ def _get_current_username(self):
+ if self._current_username is _NOT_SET:
+ self._current_username = get_ayon_username()
+ return self._current_username
+
class PublishWorkfilesModel:
"""Model for handling of published workfiles.
@@ -599,7 +640,7 @@ class PublishWorkfilesModel:
return self._cached_repre_extensions
def _file_item_from_representation(
- self, repre_entity, project_anatomy, task_name=None
+ self, repre_entity, project_anatomy, author, task_name=None
):
if task_name is not None:
task_info = repre_entity["context"].get("task")
@@ -634,6 +675,8 @@ class PublishWorkfilesModel:
dirpath,
filename,
created_at.float_timestamp,
+ author,
+ None,
repre_entity["id"]
)
@@ -643,9 +686,9 @@ class PublishWorkfilesModel:
# Get subset docs of folder
product_entities = ayon_api.get_products(
project_name,
- folder_ids=[folder_id],
- product_types=["workfile"],
- fields=["id", "name"]
+ folder_ids={folder_id},
+ product_types={"workfile"},
+ fields={"id", "name"}
)
output = []
@@ -657,25 +700,33 @@ class PublishWorkfilesModel:
version_entities = ayon_api.get_versions(
project_name,
product_ids=product_ids,
- fields=["id", "productId"]
+ fields={"id", "author"}
)
- version_ids = {version["id"] for version in version_entities}
- if not version_ids:
+ versions_by_id = {
+ version["id"]: version
+ for version in version_entities
+ }
+ if not versions_by_id:
return output
# Query representations of filtered versions and add filter for
# extension
repre_entities = ayon_api.get_representations(
project_name,
- version_ids=version_ids
+ version_ids=set(versions_by_id)
)
project_anatomy = self._controller.project_anatomy
# Filter queried representations by task name if task is set
file_items = []
for repre_entity in repre_entities:
+ version_id = repre_entity["versionId"]
+ version_entity = versions_by_id[version_id]
file_item = self._file_item_from_representation(
- repre_entity, project_anatomy, task_name
+ repre_entity,
+ project_anatomy,
+ version_entity["author"],
+ task_name,
)
if file_item is not None:
file_items.append(file_item)
diff --git a/client/ayon_core/tools/workfiles/widgets/files_widget_published.py b/client/ayon_core/tools/workfiles/widgets/files_widget_published.py
index bf36d790e9..2ce8569a9b 100644
--- a/client/ayon_core/tools/workfiles/widgets/files_widget_published.py
+++ b/client/ayon_core/tools/workfiles/widgets/files_widget_published.py
@@ -13,7 +13,8 @@ from .utils import BaseOverlayFrame
REPRE_ID_ROLE = QtCore.Qt.UserRole + 1
FILEPATH_ROLE = QtCore.Qt.UserRole + 2
-DATE_MODIFIED_ROLE = QtCore.Qt.UserRole + 3
+AUTHOR_ROLE = QtCore.Qt.UserRole + 3
+DATE_MODIFIED_ROLE = QtCore.Qt.UserRole + 4
class PublishedFilesModel(QtGui.QStandardItemModel):
@@ -23,13 +24,19 @@ class PublishedFilesModel(QtGui.QStandardItemModel):
controller (AbstractWorkfilesFrontend): The control object.
"""
+ columns = [
+ "Name",
+ "Author",
+ "Date Modified",
+ ]
+ date_modified_col = columns.index("Date Modified")
+
def __init__(self, controller):
super(PublishedFilesModel, self).__init__()
- self.setColumnCount(2)
-
- self.setHeaderData(0, QtCore.Qt.Horizontal, "Name")
- self.setHeaderData(1, QtCore.Qt.Horizontal, "Date Modified")
+ self.setColumnCount(len(self.columns))
+ for idx, label in enumerate(self.columns):
+ self.setHeaderData(idx, QtCore.Qt.Horizontal, label)
controller.register_event_callback(
"selection.task.changed",
@@ -185,6 +192,8 @@ class PublishedFilesModel(QtGui.QStandardItemModel):
self._remove_empty_item()
self._remove_missing_context_item()
+ user_items_by_name = self._controller.get_user_items_by_name()
+
items_to_remove = set(self._items_by_id.keys())
new_items = []
for file_item in file_items:
@@ -205,8 +214,15 @@ class PublishedFilesModel(QtGui.QStandardItemModel):
else:
flags = QtCore.Qt.NoItemFlags
+ author = file_item.created_by
+ user_item = user_items_by_name.get(author)
+ if user_item is not None and user_item.full_name:
+ author = user_item.full_name
+
item.setFlags(flags)
+
item.setData(file_item.filepath, FILEPATH_ROLE)
+ item.setData(author, AUTHOR_ROLE)
item.setData(file_item.modified, DATE_MODIFIED_ROLE)
self._items_by_id[repre_id] = item
@@ -225,22 +241,30 @@ class PublishedFilesModel(QtGui.QStandardItemModel):
# Use flags of first column for all columns
if index.column() != 0:
index = self.index(index.row(), 0, index.parent())
- return super(PublishedFilesModel, self).flags(index)
+ return super().flags(index)
def data(self, index, role=None):
if role is None:
role = QtCore.Qt.DisplayRole
# Handle roles for first column
- if index.column() == 1:
- if role == QtCore.Qt.DecorationRole:
- return None
+ col = index.column()
+ if col != 1:
+ return super().data(index, role)
- if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):
+ if role == QtCore.Qt.DecorationRole:
+ return None
+
+ if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):
+ if col == 1:
+ role = AUTHOR_ROLE
+ elif col == 2:
role = DATE_MODIFIED_ROLE
- index = self.index(index.row(), 0, index.parent())
+ else:
+ return None
+ index = self.index(index.row(), 0, index.parent())
- return super(PublishedFilesModel, self).data(index, role)
+ return super().data(index, role)
class SelectContextOverlay(BaseOverlayFrame):
@@ -295,7 +319,7 @@ class PublishedFilesWidget(QtWidgets.QWidget):
view.setModel(proxy_model)
time_delegate = PrettyTimeDelegate()
- view.setItemDelegateForColumn(1, time_delegate)
+ view.setItemDelegateForColumn(model.date_modified_col, time_delegate)
# Default to a wider first filename column it is what we mostly care
# about and the date modified is relatively small anyway.
diff --git a/client/ayon_core/tools/workfiles/widgets/files_widget_workarea.py b/client/ayon_core/tools/workfiles/widgets/files_widget_workarea.py
index fe6abee951..5c102dcdd4 100644
--- a/client/ayon_core/tools/workfiles/widgets/files_widget_workarea.py
+++ b/client/ayon_core/tools/workfiles/widgets/files_widget_workarea.py
@@ -10,7 +10,8 @@ from ayon_core.tools.utils.delegates import PrettyTimeDelegate
FILENAME_ROLE = QtCore.Qt.UserRole + 1
FILEPATH_ROLE = QtCore.Qt.UserRole + 2
-DATE_MODIFIED_ROLE = QtCore.Qt.UserRole + 3
+AUTHOR_ROLE = QtCore.Qt.UserRole + 3
+DATE_MODIFIED_ROLE = QtCore.Qt.UserRole + 4
class WorkAreaFilesModel(QtGui.QStandardItemModel):
@@ -21,14 +22,20 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel):
"""
refreshed = QtCore.Signal()
+ columns = [
+ "Name",
+ "Author",
+ "Date Modified",
+ ]
+ date_modified_col = columns.index("Date Modified")
def __init__(self, controller):
super(WorkAreaFilesModel, self).__init__()
- self.setColumnCount(2)
+ self.setColumnCount(len(self.columns))
- self.setHeaderData(0, QtCore.Qt.Horizontal, "Name")
- self.setHeaderData(1, QtCore.Qt.Horizontal, "Date Modified")
+ for idx, label in enumerate(self.columns):
+ self.setHeaderData(idx, QtCore.Qt.Horizontal, label)
controller.register_event_callback(
"selection.folder.changed",
@@ -186,6 +193,7 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel):
return
self._remove_empty_item()
self._remove_missing_context_item()
+ user_items_by_name = self._controller.get_user_items_by_name()
items_to_remove = set(self._items_by_filename.keys())
new_items = []
@@ -205,7 +213,13 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel):
item.setData(file_item.filename, QtCore.Qt.DisplayRole)
item.setData(file_item.filename, FILENAME_ROLE)
+ updated_by = file_item.updated_by
+ user_item = user_items_by_name.get(updated_by)
+ if user_item is not None and user_item.full_name:
+ updated_by = user_item.full_name
+
item.setData(file_item.filepath, FILEPATH_ROLE)
+ item.setData(updated_by, AUTHOR_ROLE)
item.setData(file_item.modified, DATE_MODIFIED_ROLE)
self._items_by_filename[file_item.filename] = item
@@ -224,22 +238,30 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel):
# Use flags of first column for all columns
if index.column() != 0:
index = self.index(index.row(), 0, index.parent())
- return super(WorkAreaFilesModel, self).flags(index)
+ return super().flags(index)
def data(self, index, role=None):
if role is None:
role = QtCore.Qt.DisplayRole
# Handle roles for first column
- if index.column() == 1:
- if role == QtCore.Qt.DecorationRole:
- return None
+ col = index.column()
+ if col == 0:
+ return super().data(index, role)
- if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):
+ if role == QtCore.Qt.DecorationRole:
+ return None
+
+ if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):
+ if col == 1:
+ role = AUTHOR_ROLE
+ elif col == 2:
role = DATE_MODIFIED_ROLE
- index = self.index(index.row(), 0, index.parent())
+ else:
+ return None
+ index = self.index(index.row(), 0, index.parent())
- return super(WorkAreaFilesModel, self).data(index, role)
+ return super().data(index, role)
def set_published_mode(self, published_mode):
if self._published_mode == published_mode:
@@ -279,7 +301,7 @@ class WorkAreaFilesWidget(QtWidgets.QWidget):
view.setModel(proxy_model)
time_delegate = PrettyTimeDelegate()
- view.setItemDelegateForColumn(1, time_delegate)
+ view.setItemDelegateForColumn(model.date_modified_col, time_delegate)
# Default to a wider first filename column it is what we mostly care
# about and the date modified is relatively small anyway.
diff --git a/client/ayon_core/tools/workfiles/widgets/side_panel.py b/client/ayon_core/tools/workfiles/widgets/side_panel.py
index 5085f4701e..53fdf0e0ac 100644
--- a/client/ayon_core/tools/workfiles/widgets/side_panel.py
+++ b/client/ayon_core/tools/workfiles/widgets/side_panel.py
@@ -147,13 +147,38 @@ class SidePanelWidget(QtWidgets.QWidget):
workfile_info.creation_time)
modification_time = datetime.datetime.fromtimestamp(
workfile_info.modification_time)
+
+ user_items_by_name = self._controller.get_user_items_by_name()
+
+ def convert_username(username):
+ user_item = user_items_by_name.get(username)
+ if user_item is not None and user_item.full_name:
+ return user_item.full_name
+ return username
+
+ created_lines = [
+ creation_time.strftime(datetime_format)
+ ]
+ if workfile_info.created_by:
+ created_lines.insert(
+ 0, convert_username(workfile_info.created_by)
+ )
+
+ modified_lines = [
+ modification_time.strftime(datetime_format)
+ ]
+ if workfile_info.updated_by:
+ modified_lines.insert(
+ 0, convert_username(workfile_info.updated_by)
+ )
+
lines = (
"Size:",
size_value,
"Created:",
- creation_time.strftime(datetime_format),
+ "
".join(created_lines),
"Modified:",
- modification_time.strftime(datetime_format)
+ "
".join(modified_lines),
)
self._orig_note = note
self._note_input.setPlainText(note)
diff --git a/client/ayon_core/tools/workfiles/widgets/window.py b/client/ayon_core/tools/workfiles/widgets/window.py
index 1cfae7ec90..8bcff66f50 100644
--- a/client/ayon_core/tools/workfiles/widgets/window.py
+++ b/client/ayon_core/tools/workfiles/widgets/window.py
@@ -107,7 +107,7 @@ class WorkfilesToolWindow(QtWidgets.QWidget):
split_widget.addWidget(tasks_widget)
split_widget.addWidget(col_3_widget)
split_widget.addWidget(side_panel)
- split_widget.setSizes([255, 160, 455, 175])
+ split_widget.setSizes([255, 175, 550, 190])
body_layout.addWidget(split_widget)
@@ -169,7 +169,7 @@ class WorkfilesToolWindow(QtWidgets.QWidget):
# Force focus on the open button by default, required for Houdini.
self._files_widget.setFocus()
- self.resize(1200, 600)
+ self.resize(1260, 600)
def _create_col_1_widget(self, controller, parent):
col_widget = QtWidgets.QWidget(parent)
diff --git a/server/__init__.py b/server/__init__.py
index 79f505ccd5..d60f50f471 100644
--- a/server/__init__.py
+++ b/server/__init__.py
@@ -2,7 +2,11 @@ from typing import Any
from ayon_server.addons import BaseServerAddon
-from .settings import CoreSettings, DEFAULT_VALUES
+from .settings import (
+ CoreSettings,
+ DEFAULT_VALUES,
+ convert_settings_overrides,
+)
class CoreAddon(BaseServerAddon):
@@ -17,47 +21,8 @@ class CoreAddon(BaseServerAddon):
source_version: str,
overrides: dict[str, Any],
) -> dict[str, Any]:
- self._convert_imagio_configs_0_3_1(overrides)
+ convert_settings_overrides(source_version, overrides)
# Use super conversion
return await super().convert_settings_overrides(
source_version, overrides
)
-
- def _convert_imagio_configs_0_3_1(self, overrides):
- """Imageio config settings did change to profiles since 0.3.1. ."""
- imageio_overrides = overrides.get("imageio") or {}
- if (
- "ocio_config" not in imageio_overrides
- or "filepath" not in imageio_overrides["ocio_config"]
- ):
- return
-
- ocio_config = imageio_overrides.pop("ocio_config")
-
- filepath = ocio_config["filepath"]
- if not filepath:
- return
- first_filepath = filepath[0]
- ocio_config_profiles = imageio_overrides.setdefault(
- "ocio_config_profiles", []
- )
- base_value = {
- "type": "builtin_path",
- "product_name": "",
- "host_names": [],
- "task_names": [],
- "task_types": [],
- "custom_path": "",
- "builtin_path": "{BUILTIN_OCIO_ROOT}/aces_1.2/config.ocio"
- }
- if first_filepath in (
- "{BUILTIN_OCIO_ROOT}/aces_1.2/config.ocio",
- "{BUILTIN_OCIO_ROOT}/nuke-default/config.ocio",
- ):
- base_value["type"] = "builtin_path"
- base_value["builtin_path"] = first_filepath
- else:
- base_value["type"] = "custom_path"
- base_value["custom_path"] = first_filepath
-
- ocio_config_profiles.append(base_value)
diff --git a/server/settings/__init__.py b/server/settings/__init__.py
index 527a2bdc0c..4bb21a9644 100644
--- a/server/settings/__init__.py
+++ b/server/settings/__init__.py
@@ -1,7 +1,10 @@
from .main import CoreSettings, DEFAULT_VALUES
+from .conversion import convert_settings_overrides
__all__ = (
"CoreSettings",
"DEFAULT_VALUES",
+
+ "convert_settings_overrides",
)
diff --git a/server/settings/conversion.py b/server/settings/conversion.py
new file mode 100644
index 0000000000..f513738603
--- /dev/null
+++ b/server/settings/conversion.py
@@ -0,0 +1,86 @@
+import copy
+from typing import Any
+
+from .publish_plugins import DEFAULT_PUBLISH_VALUES
+
+
+def _convert_imageio_configs_0_3_1(overrides):
+ """Imageio config settings did change to profiles since 0.3.1. ."""
+ imageio_overrides = overrides.get("imageio") or {}
+ if (
+ "ocio_config" not in imageio_overrides
+ or "filepath" not in imageio_overrides["ocio_config"]
+ ):
+ return
+
+ ocio_config = imageio_overrides.pop("ocio_config")
+
+ filepath = ocio_config["filepath"]
+ if not filepath:
+ return
+ first_filepath = filepath[0]
+ ocio_config_profiles = imageio_overrides.setdefault(
+ "ocio_config_profiles", []
+ )
+ base_value = {
+ "type": "builtin_path",
+ "product_name": "",
+ "host_names": [],
+ "task_names": [],
+ "task_types": [],
+ "custom_path": "",
+ "builtin_path": "{BUILTIN_OCIO_ROOT}/aces_1.2/config.ocio"
+ }
+ if first_filepath in (
+ "{BUILTIN_OCIO_ROOT}/aces_1.2/config.ocio",
+ "{BUILTIN_OCIO_ROOT}/nuke-default/config.ocio",
+ ):
+ base_value["type"] = "builtin_path"
+ base_value["builtin_path"] = first_filepath
+ else:
+ base_value["type"] = "custom_path"
+ base_value["custom_path"] = first_filepath
+
+ ocio_config_profiles.append(base_value)
+
+
+def _convert_validate_version_0_3_3(publish_overrides):
+ """ValidateVersion plugin changed in 0.3.3."""
+ if "ValidateVersion" not in publish_overrides:
+ return
+
+ validate_version = publish_overrides["ValidateVersion"]
+ # Already new settings
+ if "plugin_state_profiles" in validate_version:
+ return
+
+ # Use new default profile as base
+ profile = copy.deepcopy(
+ DEFAULT_PUBLISH_VALUES["ValidateVersion"]["plugin_state_profiles"][0]
+ )
+ # Copy values from old overrides to new overrides
+ for key in {
+ "enabled",
+ "optional",
+ "active",
+ }:
+ if key not in validate_version:
+ continue
+ profile[key] = validate_version.pop(key)
+
+ validate_version["plugin_state_profiles"] = [profile]
+
+
+def _conver_publish_plugins(overrides):
+ if "publish" not in overrides:
+ return
+ _convert_validate_version_0_3_3(overrides["publish"])
+
+
+def convert_settings_overrides(
+ source_version: str,
+ overrides: dict[str, Any],
+) -> dict[str, Any]:
+ _convert_imageio_configs_0_3_1(overrides)
+ _conver_publish_plugins(overrides)
+ return overrides
diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py
index 61e73ce912..ef531c8345 100644
--- a/server/settings/publish_plugins.py
+++ b/server/settings/publish_plugins.py
@@ -59,7 +59,7 @@ class CollectFramesFixDefModel(BaseSettingsModel):
)
-class ValidateOutdatedContainersProfile(BaseSettingsModel):
+class PluginStateByHostModelProfile(BaseSettingsModel):
_layout = "expanded"
# Filtering
host_names: list[str] = SettingsField(
@@ -72,17 +72,12 @@ class ValidateOutdatedContainersProfile(BaseSettingsModel):
active: bool = SettingsField(True, title="Active")
-class ValidateOutdatedContainersModel(BaseSettingsModel):
- """Validate if Publishing intent was selected.
-
- It is possible to disable validation for specific publishing context
- with profiles.
- """
-
+class PluginStateByHostModel(BaseSettingsModel):
_isGroup = True
- plugin_state_profiles: list[ValidateOutdatedContainersProfile] = SettingsField(
+ plugin_state_profiles: list[PluginStateByHostModelProfile] = SettingsField(
default_factory=list,
title="Plugin enable state profiles",
+ description="Change plugin state based on host name."
)
@@ -793,12 +788,16 @@ class PublishPuginsModel(BaseSettingsModel):
default_factory=ValidateBaseModel,
title="Validate Editorial Asset Name"
)
- ValidateVersion: ValidateBaseModel = SettingsField(
- default_factory=ValidateBaseModel,
- title="Validate Version"
+ ValidateVersion: PluginStateByHostModel = SettingsField(
+ default_factory=PluginStateByHostModel,
+ title="Validate Version",
+ description=(
+ "Validate that product version to integrate"
+ " is newer than latest version in AYON."
+ )
)
- ValidateOutdatedContainers: ValidateOutdatedContainersModel = SettingsField(
- default_factory=ValidateOutdatedContainersModel,
+ ValidateOutdatedContainers: PluginStateByHostModel = SettingsField(
+ default_factory=PluginStateByHostModel,
title="Validate Containers"
)
ValidateIntent: ValidateIntentModel = SettingsField(
@@ -882,9 +881,21 @@ DEFAULT_PUBLISH_VALUES = {
"active": True
},
"ValidateVersion": {
- "enabled": True,
- "optional": False,
- "active": True
+ "plugin_state_profiles": [
+ {
+ "host_names": [
+ "aftereffects",
+ "blender",
+ "houdini",
+ "maya",
+ "nuke",
+ "photoshop",
+ ],
+ "enabled": True,
+ "optional": False,
+ "active": True
+ }
+ ]
},
"ValidateOutdatedContainers": {
"plugin_state_profiles": [