mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 12:54:40 +01:00
Merge branch 'develop' of https://github.com/ynput/ayon-core into enhancement/AY-5539_define-creators-per-task
This commit is contained in:
commit
62f43bfb6d
14 changed files with 450 additions and 117 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
)
|
||||
|
|
|
|||
84
client/ayon_core/tools/common_models/users.py
Normal file
84
client/ayon_core/tools/common_models/users.py
Normal file
|
|
@ -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()
|
||||
])
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 = (
|
||||
"<b>Size:</b>",
|
||||
size_value,
|
||||
"<b>Created:</b>",
|
||||
creation_time.strftime(datetime_format),
|
||||
"<br/>".join(created_lines),
|
||||
"<b>Modified:</b>",
|
||||
modification_time.strftime(datetime_format)
|
||||
"<br/>".join(modified_lines),
|
||||
)
|
||||
self._orig_note = note
|
||||
self._note_input.setPlainText(note)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
from .main import CoreSettings, DEFAULT_VALUES
|
||||
from .conversion import convert_settings_overrides
|
||||
|
||||
|
||||
__all__ = (
|
||||
"CoreSettings",
|
||||
"DEFAULT_VALUES",
|
||||
|
||||
"convert_settings_overrides",
|
||||
)
|
||||
|
|
|
|||
86
server/settings/conversion.py
Normal file
86
server/settings/conversion.py
Normal file
|
|
@ -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
|
||||
|
|
@ -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": [
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue