mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 12:54:40 +01:00
Merge remote-tracking branch 'origin/feature/909-define-basic-trait-type-using-dataclasses' into feature/911-new-traits-based-integrator
This commit is contained in:
commit
d77eab4ae7
39 changed files with 1591 additions and 76 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -84,3 +84,6 @@ mypy.ini
|
|||
poetry.lock
|
||||
|
||||
.github_changelog_generator
|
||||
|
||||
# ignore mkdocs build
|
||||
site/
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from .local_settings import (
|
|||
AYONSettingsRegistry,
|
||||
get_launcher_local_dir,
|
||||
get_launcher_storage_dir,
|
||||
get_addons_resources_dir,
|
||||
get_local_site_id,
|
||||
get_ayon_username,
|
||||
)
|
||||
|
|
@ -142,6 +143,7 @@ __all__ = [
|
|||
"AYONSettingsRegistry",
|
||||
"get_launcher_local_dir",
|
||||
"get_launcher_storage_dir",
|
||||
"get_addons_resources_dir",
|
||||
"get_local_site_id",
|
||||
"get_ayon_username",
|
||||
|
||||
|
|
|
|||
|
|
@ -96,6 +96,30 @@ def get_launcher_local_dir(*subdirs: str) -> str:
|
|||
return os.path.join(storage_dir, *subdirs)
|
||||
|
||||
|
||||
def get_addons_resources_dir(addon_name: str, *args) -> str:
|
||||
"""Get directory for storing resources for addons.
|
||||
|
||||
Some addons might need to store ad-hoc resources that are not part of
|
||||
addon client package (e.g. because of size). Studio might define
|
||||
dedicated directory to store them with 'AYON_ADDONS_RESOURCES_DIR'
|
||||
environment variable. By default, is used 'addons_resources' in
|
||||
launcher storage (might be shared across platforms).
|
||||
|
||||
Args:
|
||||
addon_name (str): Addon name.
|
||||
*args (str): Subfolders in resources directory.
|
||||
|
||||
Returns:
|
||||
str: Path to resources directory.
|
||||
|
||||
"""
|
||||
addons_resources_dir = os.getenv("AYON_ADDONS_RESOURCES_DIR")
|
||||
if not addons_resources_dir:
|
||||
addons_resources_dir = get_launcher_storage_dir("addons_resources")
|
||||
|
||||
return os.path.join(addons_resources_dir, addon_name, *args)
|
||||
|
||||
|
||||
class AYONSecureRegistry:
|
||||
"""Store information using keyring.
|
||||
|
||||
|
|
|
|||
|
|
@ -59,7 +59,8 @@ class LocatableContent(TraitBase):
|
|||
description (str): Trait description.
|
||||
id (str): id should be namespaced trait name with version
|
||||
location (str): Location.
|
||||
is_templated (Optional[bool]): Is the location templated? Default is None.
|
||||
is_templated (Optional[bool]): Is the location templated?
|
||||
Default is None.
|
||||
"""
|
||||
|
||||
name: ClassVar[str] = "LocatableContent"
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ class DigitallySigned(TraitBase):
|
|||
description: ClassVar[str] = "Digitally signed trait."
|
||||
persistent: ClassVar[bool] = True
|
||||
|
||||
|
||||
@dataclass
|
||||
class PGPSigned(DigitallySigned):
|
||||
"""PGP signed trait.
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ class CollectAudio(pyblish.api.ContextPlugin):
|
|||
"blender",
|
||||
"houdini",
|
||||
"max",
|
||||
"circuit",
|
||||
]
|
||||
|
||||
audio_product_name = "audioMain"
|
||||
|
|
|
|||
|
|
@ -54,7 +54,8 @@ class ExtractBurnin(publish.Extractor):
|
|||
"houdini",
|
||||
"max",
|
||||
"blender",
|
||||
"unreal"
|
||||
"unreal",
|
||||
"circuit",
|
||||
]
|
||||
|
||||
optional = True
|
||||
|
|
|
|||
|
|
@ -91,7 +91,8 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
"webpublisher",
|
||||
"aftereffects",
|
||||
"flame",
|
||||
"unreal"
|
||||
"unreal",
|
||||
"circuit",
|
||||
]
|
||||
|
||||
# Supported extensions
|
||||
|
|
@ -196,7 +197,7 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
).format(repre_name))
|
||||
continue
|
||||
|
||||
input_ext = repre["ext"]
|
||||
input_ext = repre["ext"].lower()
|
||||
if input_ext.startswith("."):
|
||||
input_ext = input_ext[1:]
|
||||
|
||||
|
|
|
|||
|
|
@ -39,7 +39,8 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
|
|||
"nuke",
|
||||
"aftereffects",
|
||||
"unreal",
|
||||
"houdini"
|
||||
"houdini",
|
||||
"circuit",
|
||||
]
|
||||
enabled = False
|
||||
|
||||
|
|
|
|||
138
client/ayon_core/plugins/publish/integrate_attach_reviewable.py
Normal file
138
client/ayon_core/plugins/publish/integrate_attach_reviewable.py
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
import copy
|
||||
import pyblish.api
|
||||
from typing import List
|
||||
|
||||
from ayon_core.lib import EnumDef
|
||||
from ayon_core.pipeline import OptionalPyblishPluginMixin
|
||||
|
||||
|
||||
class AttachReviewables(
|
||||
pyblish.api.InstancePlugin, OptionalPyblishPluginMixin
|
||||
):
|
||||
"""Attach reviewable to other instances
|
||||
|
||||
This pre-integrator plugin allows instances to be 'attached to' other
|
||||
instances by moving all its representations over to the other instance.
|
||||
Even though this technically could work for any representation the current
|
||||
intent is to use for reviewables only, like e.g. `review` or `render`
|
||||
product type.
|
||||
|
||||
When the reviewable is attached to another instance, the instance itself
|
||||
will not be published as a separate entity. Instead, the representations
|
||||
will be copied/moved to the instances it is attached to.
|
||||
"""
|
||||
|
||||
families = ["render", "review"]
|
||||
order = pyblish.api.IntegratorOrder - 0.499
|
||||
label = "Attach reviewables"
|
||||
|
||||
settings_category = "core"
|
||||
|
||||
def process(self, instance):
|
||||
# TODO: Support farm.
|
||||
# If instance is being submitted to the farm we should pass through
|
||||
# the 'attached reviewables' metadata to the farm job
|
||||
# TODO: Reviewable frame range and resolutions
|
||||
# Because we are attaching the data to another instance, how do we
|
||||
# correctly propagate the resolution + frame rate to the other
|
||||
# instance? Do we even need to?
|
||||
# TODO: If this were to attach 'renders' to another instance that would
|
||||
# mean there wouldn't necessarily be a render publish separate as a
|
||||
# result. Is that correct expected behavior?
|
||||
attr_values = self.get_attr_values_from_data(instance.data)
|
||||
attach_to = attr_values.get("attach", [])
|
||||
if not attach_to:
|
||||
self.log.debug(
|
||||
"Reviewable is not set to attach to another instance."
|
||||
)
|
||||
return
|
||||
|
||||
attach_instances: List[pyblish.api.Instance] = []
|
||||
for attach_instance_id in attach_to:
|
||||
# Find the `pyblish.api.Instance` matching the `CreatedInstance.id`
|
||||
# in the `attach_to` list
|
||||
attach_instance = next(
|
||||
(
|
||||
_inst
|
||||
for _inst in instance.context
|
||||
if _inst.data.get("instance_id") == attach_instance_id
|
||||
),
|
||||
None,
|
||||
)
|
||||
if attach_instance is None:
|
||||
continue
|
||||
|
||||
# Skip inactive instances
|
||||
if not attach_instance.data.get("active", True):
|
||||
continue
|
||||
|
||||
# For now do not support attaching to 'farm' instances until we
|
||||
# can pass the 'attaching' on to the farm jobs.
|
||||
if attach_instance.data.get("farm"):
|
||||
self.log.warning(
|
||||
"Attaching to farm instances is not supported yet."
|
||||
)
|
||||
continue
|
||||
|
||||
attach_instances.append(attach_instance)
|
||||
|
||||
instances_names = ", ".join(
|
||||
instance.name for instance in attach_instances
|
||||
)
|
||||
self.log.info(
|
||||
f"Attaching reviewable to other instances: {instances_names}"
|
||||
)
|
||||
|
||||
# Copy the representations of this reviewable instance to the other
|
||||
# instance
|
||||
representations = instance.data.get("representations", [])
|
||||
for attach_instance in attach_instances:
|
||||
self.log.info(f"Attaching to {attach_instance.name}")
|
||||
attach_instance.data.setdefault("representations", []).extend(
|
||||
copy.deepcopy(representations)
|
||||
)
|
||||
|
||||
# Delete representations on the reviewable instance itself
|
||||
for repre in representations:
|
||||
self.log.debug(
|
||||
"Marking representation as deleted because it was "
|
||||
f"attached to other instances instead: {repre}"
|
||||
)
|
||||
repre.setdefault("tags", []).append("delete")
|
||||
|
||||
# Stop integrator from trying to integrate this instance
|
||||
if attach_to:
|
||||
instance.data["integrate"] = False
|
||||
|
||||
@classmethod
|
||||
def get_attr_defs_for_instance(cls, create_context, instance):
|
||||
# TODO: Check if instance is actually a 'reviewable'
|
||||
# Filtering of instance, if needed, can be customized
|
||||
if not cls.instance_matches_plugin_families(instance):
|
||||
return []
|
||||
|
||||
items = []
|
||||
for other_instance in create_context.instances:
|
||||
if other_instance == instance:
|
||||
continue
|
||||
|
||||
# Do not allow attaching to other reviewable instances
|
||||
if other_instance.data["productType"] in cls.families:
|
||||
continue
|
||||
|
||||
items.append(
|
||||
{
|
||||
"label": other_instance.label,
|
||||
"value": str(other_instance.id),
|
||||
}
|
||||
)
|
||||
|
||||
return [
|
||||
EnumDef(
|
||||
"attach",
|
||||
label="Attach reviewable",
|
||||
multiselection=True,
|
||||
items=items,
|
||||
tooltip="Attach this reviewable to another instance",
|
||||
)
|
||||
]
|
||||
|
|
@ -227,6 +227,9 @@ class HierarchyModel(object):
|
|||
self._tasks_by_id = NestedCacheItem(
|
||||
levels=2, default_factory=dict, lifetime=self.lifetime)
|
||||
|
||||
self._entity_ids_by_assignee = NestedCacheItem(
|
||||
levels=2, default_factory=dict, lifetime=self.lifetime)
|
||||
|
||||
self._folders_refreshing = set()
|
||||
self._tasks_refreshing = set()
|
||||
self._controller = controller
|
||||
|
|
@ -238,6 +241,8 @@ class HierarchyModel(object):
|
|||
self._task_items.reset()
|
||||
self._tasks_by_id.reset()
|
||||
|
||||
self._entity_ids_by_assignee.reset()
|
||||
|
||||
def refresh_project(self, project_name):
|
||||
"""Force to refresh folder items for a project.
|
||||
|
||||
|
|
@ -461,6 +466,54 @@ class HierarchyModel(object):
|
|||
output = self.get_task_entities(project_name, {task_id})
|
||||
return output[task_id]
|
||||
|
||||
def get_entity_ids_for_assignees(
|
||||
self, project_name: str, assignees: list[str]
|
||||
):
|
||||
folder_ids = set()
|
||||
task_ids = set()
|
||||
output = {
|
||||
"folder_ids": folder_ids,
|
||||
"task_ids": task_ids,
|
||||
}
|
||||
assignees = set(assignees)
|
||||
for assignee in tuple(assignees):
|
||||
cache = self._entity_ids_by_assignee[project_name][assignee]
|
||||
if cache.is_valid:
|
||||
assignees.discard(assignee)
|
||||
assignee_data = cache.get_data()
|
||||
folder_ids.update(assignee_data["folder_ids"])
|
||||
task_ids.update(assignee_data["task_ids"])
|
||||
|
||||
if not assignees:
|
||||
return output
|
||||
|
||||
tasks = ayon_api.get_tasks(
|
||||
project_name,
|
||||
assignees_all=assignees,
|
||||
fields={"id", "folderId", "assignees"},
|
||||
)
|
||||
tasks_assignee = {}
|
||||
for task in tasks:
|
||||
folder_ids.add(task["folderId"])
|
||||
task_ids.add(task["id"])
|
||||
for assignee in task["assignees"]:
|
||||
tasks_assignee.setdefault(assignee, []).append(task)
|
||||
|
||||
for assignee, tasks in tasks_assignee.items():
|
||||
cache = self._entity_ids_by_assignee[project_name][assignee]
|
||||
assignee_folder_ids = set()
|
||||
assignee_task_ids = set()
|
||||
assignee_data = {
|
||||
"folder_ids": assignee_folder_ids,
|
||||
"task_ids": assignee_task_ids,
|
||||
}
|
||||
for task in tasks:
|
||||
assignee_folder_ids.add(task["folderId"])
|
||||
assignee_task_ids.add(task["id"])
|
||||
cache.update_data(assignee_data)
|
||||
|
||||
return output
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _folder_refresh_event_manager(self, project_name, sender):
|
||||
self._folders_refreshing.add(project_name)
|
||||
|
|
|
|||
|
|
@ -160,8 +160,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
Returns:
|
||||
list[FolderItem]: Minimum possible information needed
|
||||
for visualisation of folder hierarchy.
|
||||
"""
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
|
|
@ -180,8 +180,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
Returns:
|
||||
list[TaskItem]: Minimum possible information needed
|
||||
for visualisation of tasks.
|
||||
"""
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
|
|
@ -190,8 +190,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
|
||||
Returns:
|
||||
Union[str, None]: Selected project name.
|
||||
"""
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
|
|
@ -200,8 +200,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
|
||||
Returns:
|
||||
Union[str, None]: Selected folder id.
|
||||
"""
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
|
|
@ -210,8 +210,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
|
||||
Returns:
|
||||
Union[str, None]: Selected task id.
|
||||
"""
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
|
|
@ -220,8 +220,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
|
||||
Returns:
|
||||
Union[str, None]: Selected task name.
|
||||
"""
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
|
|
@ -238,8 +238,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
|
||||
Returns:
|
||||
dict[str, Union[str, None]]: Selected context.
|
||||
"""
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
|
|
@ -249,8 +249,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
Args:
|
||||
project_name (Union[str, None]): Project nameor None if no project
|
||||
is selected.
|
||||
"""
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
|
|
@ -260,8 +260,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
Args:
|
||||
folder_id (Union[str, None]): Folder id or None if no folder
|
||||
is selected.
|
||||
"""
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
|
|
@ -273,8 +273,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
is selected.
|
||||
task_name (Union[str, None]): Task name or None if no task
|
||||
is selected.
|
||||
"""
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
# Actions
|
||||
|
|
@ -290,8 +290,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
Returns:
|
||||
list[ActionItem]: List of action items that should be shown
|
||||
for given context.
|
||||
"""
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
|
|
@ -303,8 +303,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
folder_id (Union[str, None]): Folder id.
|
||||
task_id (Union[str, None]): Task id.
|
||||
action_id (str): Action identifier.
|
||||
"""
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
|
|
@ -317,10 +317,10 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
project_name (Union[str, None]): Project name.
|
||||
folder_id (Union[str, None]): Folder id.
|
||||
task_id (Union[str, None]): Task id.
|
||||
action_id (Iterable[str]): Action identifiers.
|
||||
action_ids (Iterable[str]): Action identifiers.
|
||||
enabled (bool): New value of force not open workfile.
|
||||
"""
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
|
|
@ -340,5 +340,17 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
Triggers 'controller.refresh.actions.started' event at the beginning
|
||||
and 'controller.refresh.actions.finished' at the end.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_my_tasks_entity_ids(self, project_name: str):
|
||||
"""Get entity ids for my tasks.
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
|
||||
Returns:
|
||||
dict[str, Union[list[str]]]: Folder and task ids.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from ayon_core.lib import Logger
|
||||
from ayon_core.lib import Logger, get_ayon_username
|
||||
from ayon_core.lib.events import QueuedEventSystem
|
||||
from ayon_core.settings import get_project_settings
|
||||
from ayon_core.tools.common_models import ProjectsModel, HierarchyModel
|
||||
|
|
@ -6,6 +6,8 @@ from ayon_core.tools.common_models import ProjectsModel, HierarchyModel
|
|||
from .abstract import AbstractLauncherFrontEnd, AbstractLauncherBackend
|
||||
from .models import LauncherSelectionModel, ActionsModel
|
||||
|
||||
NOT_SET = object()
|
||||
|
||||
|
||||
class BaseLauncherController(
|
||||
AbstractLauncherFrontEnd, AbstractLauncherBackend
|
||||
|
|
@ -15,6 +17,8 @@ class BaseLauncherController(
|
|||
self._event_system = None
|
||||
self._log = None
|
||||
|
||||
self._username = NOT_SET
|
||||
|
||||
self._selection_model = LauncherSelectionModel(self)
|
||||
self._projects_model = ProjectsModel(self)
|
||||
self._hierarchy_model = HierarchyModel(self)
|
||||
|
|
@ -168,5 +172,19 @@ class BaseLauncherController(
|
|||
|
||||
self._emit_event("controller.refresh.actions.finished")
|
||||
|
||||
def get_my_tasks_entity_ids(self, project_name: str):
|
||||
username = self._get_my_username()
|
||||
assignees = []
|
||||
if username:
|
||||
assignees.append(username)
|
||||
return self._hierarchy_model.get_entity_ids_for_assignees(
|
||||
project_name, assignees
|
||||
)
|
||||
|
||||
def _get_my_username(self):
|
||||
if self._username is NOT_SET:
|
||||
self._username = get_ayon_username()
|
||||
return self._username
|
||||
|
||||
def _emit_event(self, topic, data=None):
|
||||
self.emit_event(topic, data, "controller")
|
||||
|
|
|
|||
|
|
@ -5,17 +5,17 @@ from ayon_core.tools.utils import (
|
|||
PlaceholderLineEdit,
|
||||
SquareButton,
|
||||
RefreshButton,
|
||||
)
|
||||
from ayon_core.tools.utils import (
|
||||
ProjectsCombobox,
|
||||
FoldersWidget,
|
||||
TasksWidget,
|
||||
NiceCheckbox,
|
||||
)
|
||||
from ayon_core.tools.utils.lib import checkstate_int_to_enum
|
||||
|
||||
|
||||
class HierarchyPage(QtWidgets.QWidget):
|
||||
def __init__(self, controller, parent):
|
||||
super(HierarchyPage, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
|
||||
# Header
|
||||
header_widget = QtWidgets.QWidget(self)
|
||||
|
|
@ -43,23 +43,36 @@ class HierarchyPage(QtWidgets.QWidget):
|
|||
)
|
||||
content_body.setOrientation(QtCore.Qt.Horizontal)
|
||||
|
||||
# - Folders widget with filter
|
||||
folders_wrapper = QtWidgets.QWidget(content_body)
|
||||
# - filters
|
||||
filters_widget = QtWidgets.QWidget(self)
|
||||
|
||||
folders_filter_text = PlaceholderLineEdit(folders_wrapper)
|
||||
folders_filter_text = PlaceholderLineEdit(filters_widget)
|
||||
folders_filter_text.setPlaceholderText("Filter folders...")
|
||||
|
||||
folders_widget = FoldersWidget(controller, folders_wrapper)
|
||||
my_tasks_tooltip = (
|
||||
"Filter folders and task to only those you are assigned to."
|
||||
)
|
||||
my_tasks_label = QtWidgets.QLabel("My tasks", filters_widget)
|
||||
my_tasks_label.setToolTip(my_tasks_tooltip)
|
||||
|
||||
folders_wrapper_layout = QtWidgets.QVBoxLayout(folders_wrapper)
|
||||
folders_wrapper_layout.setContentsMargins(0, 0, 0, 0)
|
||||
folders_wrapper_layout.addWidget(folders_filter_text, 0)
|
||||
folders_wrapper_layout.addWidget(folders_widget, 1)
|
||||
my_tasks_checkbox = NiceCheckbox(filters_widget)
|
||||
my_tasks_checkbox.setChecked(False)
|
||||
my_tasks_checkbox.setToolTip(my_tasks_tooltip)
|
||||
|
||||
filters_layout = QtWidgets.QHBoxLayout(filters_widget)
|
||||
filters_layout.setContentsMargins(0, 0, 0, 0)
|
||||
filters_layout.addWidget(folders_filter_text, 1)
|
||||
filters_layout.addWidget(my_tasks_label, 0)
|
||||
filters_layout.addWidget(my_tasks_checkbox, 0)
|
||||
|
||||
# - Folders widget
|
||||
folders_widget = FoldersWidget(controller, content_body)
|
||||
folders_widget.set_header_visible(True)
|
||||
|
||||
# - Tasks widget
|
||||
tasks_widget = TasksWidget(controller, content_body)
|
||||
|
||||
content_body.addWidget(folders_wrapper)
|
||||
content_body.addWidget(folders_widget)
|
||||
content_body.addWidget(tasks_widget)
|
||||
content_body.setStretchFactor(0, 100)
|
||||
content_body.setStretchFactor(1, 65)
|
||||
|
|
@ -67,20 +80,27 @@ class HierarchyPage(QtWidgets.QWidget):
|
|||
main_layout = QtWidgets.QVBoxLayout(self)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
main_layout.addWidget(header_widget, 0)
|
||||
main_layout.addWidget(filters_widget, 0)
|
||||
main_layout.addWidget(content_body, 1)
|
||||
|
||||
btn_back.clicked.connect(self._on_back_clicked)
|
||||
refresh_btn.clicked.connect(self._on_refresh_clicked)
|
||||
folders_filter_text.textChanged.connect(self._on_filter_text_changed)
|
||||
my_tasks_checkbox.stateChanged.connect(
|
||||
self._on_my_tasks_checkbox_state_changed
|
||||
)
|
||||
|
||||
self._is_visible = False
|
||||
self._controller = controller
|
||||
|
||||
self._btn_back = btn_back
|
||||
self._projects_combobox = projects_combobox
|
||||
self._my_tasks_checkbox = my_tasks_checkbox
|
||||
self._folders_widget = folders_widget
|
||||
self._tasks_widget = tasks_widget
|
||||
|
||||
self._project_name = None
|
||||
|
||||
# Post init
|
||||
projects_combobox.set_listen_to_selection_change(self._is_visible)
|
||||
|
||||
|
|
@ -91,10 +111,14 @@ class HierarchyPage(QtWidgets.QWidget):
|
|||
self._projects_combobox.set_listen_to_selection_change(visible)
|
||||
if visible and project_name:
|
||||
self._projects_combobox.set_selection(project_name)
|
||||
self._project_name = project_name
|
||||
|
||||
def refresh(self):
|
||||
self._folders_widget.refresh()
|
||||
self._tasks_widget.refresh()
|
||||
self._on_my_tasks_checkbox_state_changed(
|
||||
self._my_tasks_checkbox.checkState()
|
||||
)
|
||||
|
||||
def _on_back_clicked(self):
|
||||
self._controller.set_selected_project(None)
|
||||
|
|
@ -104,3 +128,16 @@ class HierarchyPage(QtWidgets.QWidget):
|
|||
|
||||
def _on_filter_text_changed(self, text):
|
||||
self._folders_widget.set_name_filter(text)
|
||||
|
||||
def _on_my_tasks_checkbox_state_changed(self, state):
|
||||
folder_ids = None
|
||||
task_ids = None
|
||||
state = checkstate_int_to_enum(state)
|
||||
if state == QtCore.Qt.Checked:
|
||||
entity_ids = self._controller.get_my_tasks_entity_ids(
|
||||
self._project_name
|
||||
)
|
||||
folder_ids = entity_ids["folder_ids"]
|
||||
task_ids = entity_ids["task_ids"]
|
||||
self._folders_widget.set_folder_ids_filter(folder_ids)
|
||||
self._tasks_widget.set_task_ids_filter(task_ids)
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ class LauncherWindow(QtWidgets.QWidget):
|
|||
page_side_anim_interval = 250
|
||||
|
||||
def __init__(self, controller=None, parent=None):
|
||||
super(LauncherWindow, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
|
||||
if controller is None:
|
||||
controller = BaseLauncherController()
|
||||
|
|
@ -153,14 +153,14 @@ class LauncherWindow(QtWidgets.QWidget):
|
|||
self.resize(520, 740)
|
||||
|
||||
def showEvent(self, event):
|
||||
super(LauncherWindow, self).showEvent(event)
|
||||
super().showEvent(event)
|
||||
self._window_is_active = True
|
||||
if not self._actions_refresh_timer.isActive():
|
||||
self._actions_refresh_timer.start()
|
||||
self._controller.refresh()
|
||||
|
||||
def closeEvent(self, event):
|
||||
super(LauncherWindow, self).closeEvent(event)
|
||||
super().closeEvent(event)
|
||||
self._window_is_active = False
|
||||
self._actions_refresh_timer.stop()
|
||||
|
||||
|
|
@ -176,7 +176,7 @@ class LauncherWindow(QtWidgets.QWidget):
|
|||
self._on_actions_refresh_timeout()
|
||||
self._actions_refresh_timer.start()
|
||||
|
||||
super(LauncherWindow, self).changeEvent(event)
|
||||
super().changeEvent(event)
|
||||
|
||||
def _on_actions_refresh_timeout(self):
|
||||
# Stop timer if widget is not visible
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from __future__ import annotations
|
||||
from qtpy import QtGui, QtCore
|
||||
|
||||
from ._multicombobox import (
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
from __future__ import annotations
|
||||
import collections
|
||||
from typing import Optional
|
||||
|
||||
from qtpy import QtWidgets, QtGui, QtCore
|
||||
|
||||
|
|
@ -33,7 +35,10 @@ class FoldersQtModel(QtGui.QStandardItemModel):
|
|||
refreshed = QtCore.Signal()
|
||||
|
||||
def __init__(self, controller):
|
||||
super(FoldersQtModel, self).__init__()
|
||||
super().__init__()
|
||||
|
||||
self.setColumnCount(1)
|
||||
self.setHeaderData(0, QtCore.Qt.Horizontal, "Folders")
|
||||
|
||||
self._controller = controller
|
||||
self._items_by_id = {}
|
||||
|
|
@ -334,6 +339,29 @@ class FoldersQtModel(QtGui.QStandardItemModel):
|
|||
self.refreshed.emit()
|
||||
|
||||
|
||||
class FoldersProxyModel(RecursiveSortFilterProxyModel):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._folder_ids_filter = None
|
||||
|
||||
def set_folder_ids_filter(self, folder_ids: Optional[list[str]]):
|
||||
if self._folder_ids_filter == folder_ids:
|
||||
return
|
||||
self._folder_ids_filter = folder_ids
|
||||
self.invalidateFilter()
|
||||
|
||||
def filterAcceptsRow(self, row, parent_index):
|
||||
if self._folder_ids_filter is not None:
|
||||
if not self._folder_ids_filter:
|
||||
return False
|
||||
source_index = self.sourceModel().index(row, 0, parent_index)
|
||||
folder_id = source_index.data(FOLDER_ID_ROLE)
|
||||
if folder_id not in self._folder_ids_filter:
|
||||
return False
|
||||
return super().filterAcceptsRow(row, parent_index)
|
||||
|
||||
|
||||
class FoldersWidget(QtWidgets.QWidget):
|
||||
"""Folders widget.
|
||||
|
||||
|
|
@ -369,13 +397,13 @@ class FoldersWidget(QtWidgets.QWidget):
|
|||
refreshed = QtCore.Signal()
|
||||
|
||||
def __init__(self, controller, parent, handle_expected_selection=False):
|
||||
super(FoldersWidget, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
|
||||
folders_view = TreeView(self)
|
||||
folders_view.setHeaderHidden(True)
|
||||
|
||||
folders_model = FoldersQtModel(controller)
|
||||
folders_proxy_model = RecursiveSortFilterProxyModel()
|
||||
folders_proxy_model = FoldersProxyModel()
|
||||
folders_proxy_model.setSourceModel(folders_model)
|
||||
folders_proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)
|
||||
|
||||
|
|
@ -446,6 +474,18 @@ class FoldersWidget(QtWidgets.QWidget):
|
|||
if name:
|
||||
self._folders_view.expandAll()
|
||||
|
||||
def set_folder_ids_filter(self, folder_ids: Optional[list[str]]):
|
||||
"""Set filter of folder ids.
|
||||
|
||||
Args:
|
||||
folder_ids (list[str]): The list of folder ids.
|
||||
|
||||
"""
|
||||
self._folders_proxy_model.set_folder_ids_filter(folder_ids)
|
||||
|
||||
def set_header_visible(self, visible: bool):
|
||||
self._folders_view.setHeaderHidden(not visible)
|
||||
|
||||
def refresh(self):
|
||||
"""Refresh folders model.
|
||||
|
||||
|
|
|
|||
|
|
@ -286,6 +286,7 @@ class ProjectSortFilterProxy(QtCore.QSortFilterProxyModel):
|
|||
self._sort_by_type = True
|
||||
# Disable case sensitivity
|
||||
self.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)
|
||||
self.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
|
||||
|
||||
def _type_sort(self, l_index, r_index):
|
||||
if not self._sort_by_type:
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
from __future__ import annotations
|
||||
from typing import Optional
|
||||
|
||||
from qtpy import QtWidgets, QtGui, QtCore
|
||||
|
||||
from ayon_core.style import (
|
||||
|
|
@ -343,6 +346,29 @@ class TasksQtModel(QtGui.QStandardItemModel):
|
|||
return self._has_content
|
||||
|
||||
|
||||
class TasksProxyModel(QtCore.QSortFilterProxyModel):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._task_ids_filter: Optional[set[str]] = None
|
||||
|
||||
def set_task_ids_filter(self, task_ids: Optional[set[str]]):
|
||||
if self._task_ids_filter == task_ids:
|
||||
return
|
||||
self._task_ids_filter = task_ids
|
||||
self.invalidateFilter()
|
||||
|
||||
def filterAcceptsRow(self, row, parent_index):
|
||||
if self._task_ids_filter is not None:
|
||||
if not self._task_ids_filter:
|
||||
return False
|
||||
source_index = self.sourceModel().index(row, 0, parent_index)
|
||||
task_id = source_index.data(ITEM_ID_ROLE)
|
||||
if task_id is not None and task_id not in self._task_ids_filter:
|
||||
return False
|
||||
return super().filterAcceptsRow(row, parent_index)
|
||||
|
||||
|
||||
class TasksWidget(QtWidgets.QWidget):
|
||||
"""Tasks widget.
|
||||
|
||||
|
|
@ -364,7 +390,7 @@ class TasksWidget(QtWidgets.QWidget):
|
|||
tasks_view.setIndentation(0)
|
||||
|
||||
tasks_model = TasksQtModel(controller)
|
||||
tasks_proxy_model = QtCore.QSortFilterProxyModel()
|
||||
tasks_proxy_model = TasksProxyModel()
|
||||
tasks_proxy_model.setSourceModel(tasks_model)
|
||||
tasks_proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)
|
||||
|
||||
|
|
@ -490,6 +516,15 @@ class TasksWidget(QtWidgets.QWidget):
|
|||
)
|
||||
return True
|
||||
|
||||
def set_task_ids_filter(self, task_ids: Optional[list[str]]):
|
||||
"""Set filter of folder ids.
|
||||
|
||||
Args:
|
||||
task_ids (list[str]): The list of folder ids.
|
||||
|
||||
"""
|
||||
self._tasks_proxy_model.set_task_ids_filter(task_ids)
|
||||
|
||||
def _on_tasks_refresh_finished(self, event):
|
||||
"""Tasks were refreshed in controller.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import logging
|
||||
import math
|
||||
from typing import Optional, List, Set, Any
|
||||
|
||||
from qtpy import QtWidgets, QtCore, QtGui
|
||||
|
|
@ -410,10 +411,12 @@ class ExpandingTextEdit(QtWidgets.QTextEdit):
|
|||
document = self.document().clone()
|
||||
document.setTextWidth(document_width)
|
||||
|
||||
return margins.top() + document.size().height() + margins.bottom()
|
||||
return math.ceil(
|
||||
margins.top() + document.size().height() + margins.bottom()
|
||||
)
|
||||
|
||||
def sizeHint(self):
|
||||
width = super(ExpandingTextEdit, self).sizeHint().width()
|
||||
width = super().sizeHint().width()
|
||||
return QtCore.QSize(width, self.heightForWidth(width))
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1016,6 +1016,7 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon):
|
|||
workdir,
|
||||
filename,
|
||||
template_key,
|
||||
artist_note,
|
||||
):
|
||||
"""Save current state of workfile to workarea.
|
||||
|
||||
|
|
@ -1040,6 +1041,7 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon):
|
|||
workdir,
|
||||
filename,
|
||||
template_key,
|
||||
artist_note,
|
||||
):
|
||||
"""Action to copy published workfile representation to workarea.
|
||||
|
||||
|
|
@ -1054,12 +1056,13 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon):
|
|||
workdir (str): Workarea directory.
|
||||
filename (str): Workarea filename.
|
||||
template_key (str): Template key.
|
||||
artist_note (str): Artist note.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def duplicate_workfile(self, src_filepath, workdir, filename):
|
||||
def duplicate_workfile(self, src_filepath, workdir, filename, artist_note):
|
||||
"""Duplicate workfile.
|
||||
|
||||
Workfiles is not opened when done.
|
||||
|
|
@ -1068,6 +1071,7 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon):
|
|||
src_filepath (str): Source workfile path.
|
||||
workdir (str): Destination workdir.
|
||||
filename (str): Destination filename.
|
||||
artist_note (str): Artist note.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -554,6 +554,7 @@ class BaseWorkfileController(
|
|||
workdir,
|
||||
filename,
|
||||
template_key,
|
||||
artist_note,
|
||||
):
|
||||
self._emit_event("save_as.started")
|
||||
|
||||
|
|
@ -565,6 +566,7 @@ class BaseWorkfileController(
|
|||
workdir,
|
||||
filename,
|
||||
template_key,
|
||||
artist_note=artist_note,
|
||||
)
|
||||
except Exception:
|
||||
failed = True
|
||||
|
|
@ -584,6 +586,7 @@ class BaseWorkfileController(
|
|||
workdir,
|
||||
filename,
|
||||
template_key,
|
||||
artist_note,
|
||||
):
|
||||
self._emit_event("copy_representation.started")
|
||||
|
||||
|
|
@ -595,6 +598,7 @@ class BaseWorkfileController(
|
|||
workdir,
|
||||
filename,
|
||||
template_key,
|
||||
artist_note,
|
||||
src_filepath=representation_filepath
|
||||
)
|
||||
except Exception:
|
||||
|
|
@ -608,7 +612,7 @@ class BaseWorkfileController(
|
|||
{"failed": failed},
|
||||
)
|
||||
|
||||
def duplicate_workfile(self, src_filepath, workdir, filename):
|
||||
def duplicate_workfile(self, src_filepath, workdir, filename, artist_note):
|
||||
self._emit_event("workfile_duplicate.started")
|
||||
|
||||
failed = False
|
||||
|
|
@ -701,11 +705,12 @@ class BaseWorkfileController(
|
|||
|
||||
def _save_as_workfile(
|
||||
self,
|
||||
folder_id,
|
||||
task_id,
|
||||
workdir,
|
||||
filename,
|
||||
template_key,
|
||||
folder_id: str,
|
||||
task_id: str,
|
||||
workdir: str,
|
||||
filename: str,
|
||||
template_key: str,
|
||||
artist_note: str,
|
||||
src_filepath=None,
|
||||
):
|
||||
# Trigger before save event
|
||||
|
|
@ -748,7 +753,11 @@ class BaseWorkfileController(
|
|||
self._host_save_workfile(dst_filepath)
|
||||
|
||||
# Make sure workfile info exists
|
||||
self.save_workfile_info(folder_id, task_name, dst_filepath, None)
|
||||
if not artist_note:
|
||||
artist_note = None
|
||||
self.save_workfile_info(
|
||||
folder_id, task_name, dst_filepath, note=artist_note
|
||||
)
|
||||
|
||||
# Create extra folders
|
||||
create_workdir_extra_folders(
|
||||
|
|
|
|||
|
|
@ -213,7 +213,8 @@ class FilesWidget(QtWidgets.QWidget):
|
|||
self._controller.duplicate_workfile(
|
||||
filepath,
|
||||
result["workdir"],
|
||||
result["filename"]
|
||||
result["filename"],
|
||||
artist_note=result["artist_note"]
|
||||
)
|
||||
|
||||
def _on_workarea_browse_clicked(self):
|
||||
|
|
@ -261,6 +262,7 @@ class FilesWidget(QtWidgets.QWidget):
|
|||
result["workdir"],
|
||||
result["filename"],
|
||||
result["template_key"],
|
||||
artist_note=result["artist_note"]
|
||||
)
|
||||
|
||||
def _on_workarea_path_changed(self, event):
|
||||
|
|
@ -313,6 +315,7 @@ class FilesWidget(QtWidgets.QWidget):
|
|||
result["workdir"],
|
||||
result["filename"],
|
||||
result["template_key"],
|
||||
artist_note=result["artist_note"]
|
||||
)
|
||||
|
||||
def _on_save_as_request(self):
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from qtpy import QtWidgets, QtCore
|
||||
|
||||
from ayon_core.tools.utils import PlaceholderLineEdit
|
||||
from ayon_core.tools.utils import PlaceholderLineEdit, PlaceholderPlainTextEdit
|
||||
|
||||
|
||||
class SubversionLineEdit(QtWidgets.QWidget):
|
||||
|
|
@ -143,6 +143,11 @@ class SaveAsDialog(QtWidgets.QDialog):
|
|||
version_layout.addWidget(version_input)
|
||||
version_layout.addWidget(last_version_check)
|
||||
|
||||
# Artist note widget
|
||||
artist_note_input = PlaceholderPlainTextEdit(inputs_widget)
|
||||
artist_note_input.setPlaceholderText(
|
||||
"Provide a note about this workfile.")
|
||||
|
||||
# Preview widget
|
||||
preview_widget = QtWidgets.QLabel("Preview filename", inputs_widget)
|
||||
preview_widget.setWordWrap(True)
|
||||
|
|
@ -161,6 +166,7 @@ class SaveAsDialog(QtWidgets.QDialog):
|
|||
subversion_label = QtWidgets.QLabel("Subversion:", inputs_widget)
|
||||
extension_label = QtWidgets.QLabel("Extension:", inputs_widget)
|
||||
preview_label = QtWidgets.QLabel("Preview:", inputs_widget)
|
||||
artist_note_label = QtWidgets.QLabel("Artist Note:", inputs_widget)
|
||||
|
||||
# Build inputs
|
||||
inputs_layout = QtWidgets.QGridLayout(inputs_widget)
|
||||
|
|
@ -172,6 +178,8 @@ class SaveAsDialog(QtWidgets.QDialog):
|
|||
inputs_layout.addWidget(extension_combobox, 2, 1)
|
||||
inputs_layout.addWidget(preview_label, 3, 0)
|
||||
inputs_layout.addWidget(preview_widget, 3, 1)
|
||||
inputs_layout.addWidget(artist_note_label, 4, 0, 1, 2)
|
||||
inputs_layout.addWidget(artist_note_input, 5, 0, 1, 2)
|
||||
|
||||
# Build layout
|
||||
main_layout = QtWidgets.QVBoxLayout(self)
|
||||
|
|
@ -206,11 +214,13 @@ class SaveAsDialog(QtWidgets.QDialog):
|
|||
self._extension_combobox = extension_combobox
|
||||
self._subversion_input = subversion_input
|
||||
self._preview_widget = preview_widget
|
||||
self._artist_note_input = artist_note_input
|
||||
|
||||
self._version_label = version_label
|
||||
self._subversion_label = subversion_label
|
||||
self._extension_label = extension_label
|
||||
self._preview_label = preview_label
|
||||
self._artist_note_label = artist_note_label
|
||||
|
||||
# Post init setup
|
||||
|
||||
|
|
@ -322,6 +332,7 @@ class SaveAsDialog(QtWidgets.QDialog):
|
|||
"folder_id": self._folder_id,
|
||||
"task_id": self._task_id,
|
||||
"template_key": self._template_key,
|
||||
"artist_note": self._artist_note_input.toPlainText(),
|
||||
}
|
||||
self.close()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring AYON addon 'core' version."""
|
||||
__version__ = "1.1.5+dev"
|
||||
__version__ = "1.1.6+dev"
|
||||
|
|
|
|||
12
docs/css/custom.css
Normal file
12
docs/css/custom.css
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
[data-md-color-scheme="slate"] {
|
||||
/* simple slate overrides */
|
||||
--md-primary-fg-color: hsl(155, 49%, 50%);
|
||||
--md-accent-fg-color: rgb(93, 200, 156);
|
||||
--md-typeset-a-color: hsl(155, 49%, 45%) !important;
|
||||
}
|
||||
[data-md-color-scheme="default"] {
|
||||
/* simple default overrides */
|
||||
--md-primary-fg-color: hsl(155, 49%, 50%);
|
||||
--md-accent-fg-color: rgb(93, 200, 156);
|
||||
--md-typeset-a-color: hsl(155, 49%, 45%) !important;
|
||||
}
|
||||
BIN
docs/img/ay-symbol-blackw-full.png
Normal file
BIN
docs/img/ay-symbol-blackw-full.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
BIN
docs/img/favicon.ico
Normal file
BIN
docs/img/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 490 B |
1
docs/index.md
Normal file
1
docs/index.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
--8<-- "README.md"
|
||||
1
docs/license.md
Normal file
1
docs/license.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
--8<-- "LICENSE"
|
||||
71
mkdocs.yml
Normal file
71
mkdocs.yml
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
site_name: ayon-core
|
||||
repo_url: https://github.com/ynput/ayon-core
|
||||
|
||||
nav:
|
||||
- Home: index.md
|
||||
- License: license.md
|
||||
|
||||
theme:
|
||||
name: material
|
||||
palette:
|
||||
- media: "(prefers-color-scheme: dark)"
|
||||
scheme: slate
|
||||
toggle:
|
||||
icon: material/toggle-switch-off-outline
|
||||
name: Switch to light mode
|
||||
- media: "(prefers-color-scheme: light)"
|
||||
scheme: default
|
||||
toggle:
|
||||
icon: material/toggle-switch
|
||||
name: Switch to dark mode
|
||||
logo: img/ay-symbol-blackw-full.png
|
||||
favicon: img/favicon.ico
|
||||
features:
|
||||
- navigation.sections
|
||||
- navigation.path
|
||||
- navigation.prune
|
||||
|
||||
extra:
|
||||
version:
|
||||
provider: mike
|
||||
|
||||
extra_css: [css/custom.css]
|
||||
|
||||
markdown_extensions:
|
||||
- mdx_gh_links
|
||||
- pymdownx.snippets
|
||||
|
||||
plugins:
|
||||
- search
|
||||
- offline
|
||||
- mkdocs-autoapi:
|
||||
autoapi_dir: ./
|
||||
autoapi_add_nav_entry: Reference
|
||||
autoapi_ignore:
|
||||
- .*
|
||||
- docs/**/*
|
||||
- tests/**/*
|
||||
- tools/**/*
|
||||
- stubs/**/* # mocha fix
|
||||
- ./**/pythonrc.py # houdini fix
|
||||
- .*/**/*
|
||||
- ./*.py
|
||||
- mkdocstrings:
|
||||
handlers:
|
||||
python:
|
||||
paths:
|
||||
- ./
|
||||
- client/*
|
||||
- server/*
|
||||
- services/*
|
||||
- minify:
|
||||
minify_html: true
|
||||
minify_js: true
|
||||
minify_css: true
|
||||
htmlmin_opts:
|
||||
remove_comments: true
|
||||
cache_safe: true
|
||||
- mike
|
||||
|
||||
hooks:
|
||||
- mkdocs_hooks.py
|
||||
191
mkdocs_hooks.py
Normal file
191
mkdocs_hooks.py
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
import os
|
||||
from pathlib import Path
|
||||
from shutil import rmtree
|
||||
import json
|
||||
import glob
|
||||
import logging
|
||||
|
||||
TMP_FILE = "./missing_init_files.json"
|
||||
NFILES = []
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class ColorFormatter(logging.Formatter):
|
||||
grey = "\x1b[38;20m"
|
||||
green = "\x1b[32;20m"
|
||||
yellow = "\x1b[33;20m"
|
||||
red = "\x1b[31;20m"
|
||||
bold_red = "\x1b[31;1m"
|
||||
reset = "\x1b[0m"
|
||||
fmt = (
|
||||
"%(asctime)s - %(name)s - %(levelname)s - %(message)s " # noqa
|
||||
"(%(filename)s:%(lineno)d)"
|
||||
)
|
||||
|
||||
FORMATS = {
|
||||
logging.DEBUG: grey + fmt + reset,
|
||||
logging.INFO: green + fmt + reset,
|
||||
logging.WARNING: yellow + fmt + reset,
|
||||
logging.ERROR: red + fmt + reset,
|
||||
logging.CRITICAL: bold_red + fmt + reset,
|
||||
}
|
||||
|
||||
def format(self, record):
|
||||
log_fmt = self.FORMATS.get(record.levelno)
|
||||
formatter = logging.Formatter(log_fmt)
|
||||
return formatter.format(record)
|
||||
|
||||
|
||||
ch = logging.StreamHandler()
|
||||
ch.setFormatter(ColorFormatter())
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
handlers=[ch],
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def create_init_file(dirpath, msg):
|
||||
global NFILES
|
||||
ini_file = f"{dirpath}/__init__.py"
|
||||
Path(ini_file).touch()
|
||||
NFILES.append(ini_file)
|
||||
logging.info(f"{msg}: created '{ini_file}'")
|
||||
|
||||
|
||||
def create_parent_init_files(dirpath: str, rootpath: str, msg: str):
|
||||
parent_path = dirpath
|
||||
while parent_path != rootpath:
|
||||
parent_path = os.path.dirname(parent_path)
|
||||
parent_init = os.path.join(parent_path, "__init__.py")
|
||||
if not os.path.exists(parent_init):
|
||||
create_init_file(parent_path, msg)
|
||||
else:
|
||||
break
|
||||
|
||||
|
||||
def add_missing_init_files(*roots, msg=""):
|
||||
"""
|
||||
This function takes in one or more root directories as arguments and scans
|
||||
them for Python files without an `__init__.py` file. It generates a JSON
|
||||
file named `missing_init_files.json` containing the paths of these files.
|
||||
|
||||
Args:
|
||||
*roots: Variable number of root directories to scan.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
|
||||
for root in roots:
|
||||
if not os.path.exists(root):
|
||||
continue
|
||||
rootpath = os.path.abspath(root)
|
||||
for dirpath, dirs, files in os.walk(rootpath):
|
||||
if "__init__.py" in files:
|
||||
continue
|
||||
|
||||
if "." in dirpath:
|
||||
continue
|
||||
|
||||
if not glob.glob(os.path.join(dirpath, "*.py")):
|
||||
continue
|
||||
|
||||
create_init_file(dirpath, msg)
|
||||
create_parent_init_files(dirpath, rootpath, msg)
|
||||
|
||||
with open(TMP_FILE, "w") as f:
|
||||
json.dump(NFILES, f)
|
||||
|
||||
|
||||
def remove_missing_init_files(msg=""):
|
||||
"""
|
||||
This function removes temporary `__init__.py` files created in the
|
||||
`add_missing_init_files()` function. It reads the paths of these files from
|
||||
a JSON file named `missing_init_files.json`.
|
||||
|
||||
Args:
|
||||
None
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
global NFILES
|
||||
nfiles = []
|
||||
if os.path.exists(TMP_FILE):
|
||||
with open(TMP_FILE, "r") as f:
|
||||
nfiles = json.load(f)
|
||||
else:
|
||||
nfiles = NFILES
|
||||
|
||||
for file in nfiles:
|
||||
Path(file).unlink()
|
||||
logging.info(f"{msg}: removed {file}")
|
||||
|
||||
os.remove(TMP_FILE)
|
||||
NFILES = []
|
||||
|
||||
|
||||
def remove_pychache_dirs(msg=""):
|
||||
"""
|
||||
This function walks the current directory and removes all existing
|
||||
'__pycache__' directories.
|
||||
|
||||
Args:
|
||||
msg: An optional message to display during the removal process.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
nremoved = 0
|
||||
|
||||
for dirpath, dirs, files in os.walk("."):
|
||||
if "__pycache__" in dirs:
|
||||
pydir = Path(f"{dirpath}/__pycache__")
|
||||
rmtree(pydir)
|
||||
nremoved += 1
|
||||
logging.info(f"{msg}: removed '{pydir}'")
|
||||
|
||||
if not nremoved:
|
||||
logging.info(f"{msg}: no __pycache__ dirs found")
|
||||
|
||||
|
||||
# mkdocs hooks ----------------------------------------------------------------
|
||||
|
||||
|
||||
def on_startup(command, dirty):
|
||||
remove_pychache_dirs(msg="HOOK - on_startup")
|
||||
|
||||
|
||||
def on_pre_build(config):
|
||||
"""
|
||||
This function is called before the MkDocs build process begins. It adds
|
||||
temporary `__init__.py` files to directories that do not contain one, to
|
||||
make sure mkdocs doesn't ignore them.
|
||||
"""
|
||||
try:
|
||||
add_missing_init_files(
|
||||
"client",
|
||||
"server",
|
||||
"services",
|
||||
msg="HOOK - on_pre_build",
|
||||
)
|
||||
except BaseException as e:
|
||||
logging.error(e)
|
||||
remove_missing_init_files(
|
||||
msg="HOOK - on_post_build: cleaning up on error !"
|
||||
)
|
||||
raise
|
||||
|
||||
|
||||
def on_post_build(config):
|
||||
"""
|
||||
This function is called after the MkDocs build process ends. It removes
|
||||
temporary `__init__.py` files that were added in the `on_pre_build()`
|
||||
function.
|
||||
"""
|
||||
remove_missing_init_files(msg="HOOK - on_post_build")
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
name = "core"
|
||||
title = "Core"
|
||||
version = "1.1.5+dev"
|
||||
version = "1.1.6+dev"
|
||||
|
||||
client_dir = "ayon_core"
|
||||
|
||||
|
|
|
|||
838
poetry.lock
generated
838
poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
[tool.poetry]
|
||||
name = "ayon-core"
|
||||
version = "1.1.5+dev"
|
||||
version = "1.1.6+dev"
|
||||
description = ""
|
||||
authors = ["Ynput Team <team@ynput.io>"]
|
||||
readme = "README.md"
|
||||
|
|
@ -30,6 +30,17 @@ attrs = "^25.0.0"
|
|||
pyblish-base = "^1.8.7"
|
||||
clique = "^2.0.0"
|
||||
opentimelineio = "^0.17.0"
|
||||
tomlkit = "^0.13.2"
|
||||
requests = "^2.32.3"
|
||||
mkdocs-material = "^9.6.7"
|
||||
mkdocs-autoapi = "^0.4.0"
|
||||
mkdocstrings-python = "^1.16.2"
|
||||
mkdocs-minify-plugin = "^0.8.0"
|
||||
markdown-checklist = "^0.4.4"
|
||||
mdx-gh-links = "^0.4"
|
||||
pymdown-extensions = "^10.14.3"
|
||||
mike = "^2.1.3"
|
||||
mkdocstrings-shell = "^1.0.2"
|
||||
|
||||
|
||||
[tool.ruff]
|
||||
|
|
@ -97,8 +108,6 @@ ignore = [
|
|||
"S404", # subprocess module is possibly insecure
|
||||
"PLC0415", # import must be on top of the file
|
||||
"CPY001", # missing copyright header
|
||||
"UP045"
|
||||
|
||||
]
|
||||
|
||||
# Allow fix for all enabled rules (when `--fix`) is provided.
|
||||
|
|
|
|||
|
|
@ -12,6 +12,10 @@ from ayon_server.settings import (
|
|||
from ayon_server.types import ColorRGBA_uint8
|
||||
|
||||
|
||||
class EnabledModel(BaseSettingsModel):
|
||||
enabled: bool = SettingsField(True)
|
||||
|
||||
|
||||
class ValidateBaseModel(BaseSettingsModel):
|
||||
_isGroup = True
|
||||
enabled: bool = SettingsField(True)
|
||||
|
|
@ -1026,6 +1030,17 @@ class PublishPuginsModel(BaseSettingsModel):
|
|||
default_factory=IntegrateHeroVersionModel,
|
||||
title="Integrate Hero Version"
|
||||
)
|
||||
AttachReviewables: EnabledModel = SettingsField(
|
||||
default_factory=EnabledModel,
|
||||
title="Attach Reviewables",
|
||||
description=(
|
||||
"When enabled, expose an 'Attach Reviewables' attribute on review"
|
||||
" and render instances in the publisher to allow including the"
|
||||
" media to be attached to another instance.\n\n"
|
||||
"If a reviewable is attached to another instance it will not be "
|
||||
"published as a render/review product of its own."
|
||||
)
|
||||
)
|
||||
CleanUp: CleanUpModel = SettingsField(
|
||||
default_factory=CleanUpModel,
|
||||
title="Clean Up"
|
||||
|
|
@ -1410,6 +1425,9 @@ DEFAULT_PUBLISH_VALUES = {
|
|||
],
|
||||
"use_hardlinks": False
|
||||
},
|
||||
"AttachReviewables": {
|
||||
"enabled": True,
|
||||
},
|
||||
"CleanUp": {
|
||||
"paterns": [], # codespell:ignore paterns
|
||||
"remove_temp_renders": False
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ def test_file_locations_validation() -> None:
|
|||
with pytest.raises(TraitValidationError):
|
||||
file_locations_trait.validate_trait(representation)
|
||||
|
||||
# invalid representation with mutliple file locations but
|
||||
# invalid representation with multiple file locations but
|
||||
# unrelated to either Sequence or Bundle traits
|
||||
representation = Representation(name="test", traits=[
|
||||
FileLocations(file_paths=[
|
||||
|
|
|
|||
|
|
@ -21,18 +21,18 @@ REPRESENTATION_DATA: dict = {
|
|||
"file_path": Path("/path/to/file"),
|
||||
"file_size": 1024,
|
||||
"file_hash": None,
|
||||
"persistent": True,
|
||||
# "persistent": True,
|
||||
},
|
||||
Image.id: {"persistent": True},
|
||||
Image.id: {},
|
||||
PixelBased.id: {
|
||||
"display_window_width": 1920,
|
||||
"display_window_height": 1080,
|
||||
"pixel_aspect_ratio": 1.0,
|
||||
"persistent": True,
|
||||
# "persistent": True,
|
||||
},
|
||||
Planar.id: {
|
||||
"planar_configuration": "RGB",
|
||||
"persistent": True,
|
||||
# "persistent": True,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue