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": [