fixed change of context on existing instances

This commit is contained in:
Jakub Trllo 2024-02-28 16:05:03 +01:00
parent 4044feb988
commit de2f48ff54
7 changed files with 159 additions and 248 deletions

View file

@ -12,6 +12,7 @@ from abc import ABCMeta, abstractmethod
import six
import arrow
import pyblish.api
import ayon_api
from ayon_core.client import (
get_asset_by_name,
@ -70,17 +71,17 @@ class AssetDocsCache:
def __init__(self, controller):
self._controller = controller
self._full_asset_docs_by_name = {}
self._asset_docs_by_path = {}
def reset(self):
self._full_asset_docs_by_name = {}
self._asset_docs_by_path = {}
def get_full_asset_by_name(self, asset_name):
if asset_name not in self._full_asset_docs_by_name:
def get_asset_doc_by_folder_path(self, folder_path):
if folder_path not in self._asset_docs_by_path:
project_name = self._controller.project_name
full_asset_doc = get_asset_by_name(project_name, asset_name)
self._full_asset_docs_by_name[asset_name] = full_asset_doc
return copy.deepcopy(self._full_asset_docs_by_name[asset_name])
asset_doc = get_asset_by_name(project_name, folder_path)
self._asset_docs_by_path[folder_path] = asset_doc
return copy.deepcopy(self._asset_docs_by_path[folder_path])
class PublishReportMaker:
@ -951,13 +952,13 @@ class AbstractPublisherController(object):
@property
@abstractmethod
def current_asset_name(self):
"""Current context asset name.
def current_folder_path(self):
"""Current context folder path.
Returns:
Union[str, None]: Name of asset.
"""
Union[str, None]: Folder path.
"""
pass
@property
@ -1021,7 +1022,7 @@ class AbstractPublisherController(object):
pass
@abstractmethod
def get_existing_product_names(self, asset_name):
def get_existing_product_names(self, folder_path):
pass
@abstractmethod
@ -1665,7 +1666,7 @@ class PublisherController(BasePublisherController):
return self._create_context.get_current_project_name()
@property
def current_asset_name(self):
def current_folder_path(self):
"""Current context asset name defined by host.
Returns:
@ -1727,6 +1728,8 @@ class PublisherController(BasePublisherController):
# Publisher custom method
def get_folder_id_from_path(self, folder_path):
if not folder_path:
return None
folder_item = self._hierarchy_model.get_folder_item_by_path(
self.project_name, folder_path
)
@ -1734,6 +1737,34 @@ class PublisherController(BasePublisherController):
return folder_item.entity_id
return None
def get_task_names_by_folder_paths(self, folder_paths):
# TODO implement model and cache values
if not folder_paths:
return {}
folder_items = self._hierarchy_model.get_folder_items_by_paths(
self.project_name, folder_paths
)
folder_paths_by_id = {
folder_item.entity_id: folder_item.path
for folder_item in folder_items.values()
if folder_item
}
tasks = ayon_api.get_tasks(
self.project_name,
folder_ids=set(folder_paths_by_id),
fields=["name", "folderId"]
)
output = {
folder_path: set()
for folder_path in folder_paths
}
for task in tasks:
folder_path = folder_paths_by_id.get(task["folderId"])
if folder_path:
output[folder_path].add(task["name"])
return output
def are_folder_paths_valid(self, folder_paths):
if not folder_paths:
return True
@ -1761,10 +1792,12 @@ class PublisherController(BasePublisherController):
return context_title
def get_existing_product_names(self, asset_name):
def get_existing_product_names(self, folder_path):
if not folder_path:
return None
project_name = self.project_name
folder_item = self._hierarchy_model.get_folder_item_by_path(
project_name, asset_name
project_name, folder_path
)
if not folder_item:
return None
@ -2006,7 +2039,7 @@ class PublisherController(BasePublisherController):
creator_identifier,
variant,
task_name,
asset_name,
folder_path,
instance_id=None
):
"""Get product name based on passed data.
@ -2016,14 +2049,16 @@ class PublisherController(BasePublisherController):
responsible for product name creation.
variant (str): Variant value from user's input.
task_name (str): Name of task for which is instance created.
asset_name (str): Name of asset for which is instance created.
folder_path (str): Folder path for which is instance created.
instance_id (Union[str, None]): Existing instance id when product
name is updated.
"""
creator = self._creators[creator_identifier]
project_name = self.project_name
asset_doc = self._asset_docs_cache.get_full_asset_by_name(asset_name)
asset_doc = self._asset_docs_cache.get_asset_doc_by_folder_path(
folder_path
)
instance = None
if instance_id:
instance = self.instances[instance_id]

View file

@ -212,13 +212,13 @@ class QtRemotePublishController(BasePublisherController):
pass
@abstractproperty
def current_asset_name(self):
"""Current context asset name from client.
def current_folder_path(self):
"""Current context folder path from host.
Returns:
Union[str, None]: Name of asset.
"""
Union[str, None]: Folder path.
"""
pass
@abstractproperty
@ -254,7 +254,7 @@ class QtRemotePublishController(BasePublisherController):
def get_asset_hierarchy(self):
pass
def get_existing_product_names(self, asset_name):
def get_existing_product_names(self, folder_path):
pass
@property

View file

@ -1,142 +1,48 @@
import collections
from qtpy import QtWidgets, QtCore, QtGui
from ayon_core.tools.utils.assets_widget import (
get_asset_icon,
)
from ayon_core.tools.utils import (
PlaceholderLineEdit,
RecursiveSortFilterProxyModel,
)
from ayon_core.lib.events import QueuedEventSystem
from ayon_core.tools.ayon_utils.widgets import FoldersWidget
from ayon_core.tools.utils import PlaceholderLineEdit
class AssetsHierarchyModel(QtGui.QStandardItemModel):
"""Assets hierarchy model.
For selecting asset for which an instance should be created.
Uses controller to load asset hierarchy. All asset documents are stored by
their parents.
"""
class FoldersDialogController:
def __init__(self, controller):
super(AssetsHierarchyModel, self).__init__()
self._event_system = QueuedEventSystem()
self._controller = controller
self._items_by_name = {}
self._items_by_path = {}
self._items_by_asset_id = {}
@property
def event_system(self):
return self._event_system
def reset(self):
self.clear()
def emit_event(self, topic, data=None, source=None):
"""Use implemented event system to trigger event."""
self._items_by_name = {}
self._items_by_path = {}
self._items_by_asset_id = {}
# assets_by_parent_id = self._controller.get_asset_hierarchy()
assets_by_parent_id = {}
if data is None:
data = {}
self.event_system.emit(topic, data, source)
items_by_name = {}
items_by_path = {}
items_by_asset_id = {}
_queue = collections.deque()
_queue.append((self.invisibleRootItem(), None, None))
while _queue:
parent_item, parent_id, parent_path = _queue.popleft()
children = assets_by_parent_id.get(parent_id)
if not children:
continue
def register_event_callback(self, topic, callback):
self.event_system.add_callback(topic, callback)
children_by_name = {
child["name"]: child
for child in children
}
items = []
for name in sorted(children_by_name.keys()):
child = children_by_name[name]
child_id = child["_id"]
if parent_path:
child_path = "{}/{}".format(parent_path, name)
else:
child_path = "/{}".format(name)
def get_folder_items(self, project_name, sender=None):
return self._controller.get_folder_items(project_name, sender)
has_children = bool(assets_by_parent_id.get(child_id))
icon = get_asset_icon(child, has_children)
item = QtGui.QStandardItem(name)
item.setFlags(
QtCore.Qt.ItemIsEnabled
| QtCore.Qt.ItemIsSelectable
)
item.setData(icon, QtCore.Qt.DecorationRole)
item.setData(child_id, ASSET_ID_ROLE)
item.setData(name, ASSET_NAME_ROLE)
item.setData(child_path, ASSET_PATH_ROLE)
items_by_name[name] = item
items_by_path[child_path] = item
items_by_asset_id[child_id] = item
items.append(item)
_queue.append((item, child_id, child_path))
parent_item.appendRows(items)
self._items_by_name = items_by_name
self._items_by_path = items_by_path
self._items_by_asset_id = items_by_asset_id
def get_index_by_asset_id(self, asset_id):
item = self._items_by_asset_id.get(asset_id)
if item is not None:
return item.index()
return QtCore.QModelIndex()
def get_index_by_asset_name(self, asset_name):
item = self._items_by_path.get(asset_name)
if item is None:
item = self._items_by_name.get(asset_name)
if item is None:
return QtCore.QModelIndex()
return item.index()
def name_is_valid(self, item_name):
return item_name in self._items_by_path
class AssetDialogView(QtWidgets.QTreeView):
double_clicked = QtCore.Signal(QtCore.QModelIndex)
def mouseDoubleClickEvent(self, event):
index = self.indexAt(event.pos())
if index.isValid():
self.double_clicked.emit(index)
event.accept()
def set_selected_folder(self, folder_id):
pass
class AssetsDialog(QtWidgets.QDialog):
"""Dialog to select asset for a context of instance."""
"""Dialog to select folder for a context of instance."""
def __init__(self, controller, parent):
super(AssetsDialog, self).__init__(parent)
self.setWindowTitle("Select asset")
model = AssetsHierarchyModel(controller)
proxy_model = RecursiveSortFilterProxyModel()
proxy_model.setSourceModel(model)
proxy_model.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
self.setWindowTitle("Select folder")
filter_input = PlaceholderLineEdit(self)
filter_input.setPlaceholderText("Filter folders..")
asset_view = AssetDialogView(self)
asset_view.setModel(proxy_model)
asset_view.setHeaderHidden(True)
asset_view.setFrameShape(QtWidgets.QFrame.NoFrame)
asset_view.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
asset_view.setAlternatingRowColors(True)
asset_view.setSelectionBehavior(QtWidgets.QTreeView.SelectRows)
asset_view.setAllColumnsShowFocus(True)
folders_controller = FoldersDialogController(controller)
folders_widget = FoldersWidget(folders_controller, self)
ok_btn = QtWidgets.QPushButton("OK", self)
cancel_btn = QtWidgets.QPushButton("Cancel", self)
@ -148,28 +54,26 @@ class AssetsDialog(QtWidgets.QDialog):
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(filter_input, 0)
layout.addWidget(asset_view, 1)
layout.addWidget(folders_widget, 1)
layout.addLayout(btns_layout, 0)
controller.event_system.add_callback(
"controller.reset.finished", self._on_controller_reset
)
asset_view.double_clicked.connect(self._on_ok_clicked)
folders_widget.double_clicked.connect(self._on_ok_clicked)
filter_input.textChanged.connect(self._on_filter_change)
ok_btn.clicked.connect(self._on_ok_clicked)
cancel_btn.clicked.connect(self._on_cancel_clicked)
self._controller = controller
self._filter_input = filter_input
self._ok_btn = ok_btn
self._cancel_btn = cancel_btn
self._model = model
self._proxy_model = proxy_model
self._folders_widget = folders_widget
self._asset_view = asset_view
self._selected_asset = None
self._selected_folder_path = None
# Soft refresh is enabled
# - reset will happen at all cost if soft reset is enabled
# - adds ability to call reset on multiple places without repeating
@ -194,7 +98,7 @@ class AssetsDialog(QtWidgets.QDialog):
self._soft_reset_enabled = True
def showEvent(self, event):
"""Refresh asset model on show."""
"""Refresh folders widget on show."""
super(AssetsDialog, self).showEvent(event)
if self._first_show:
self._first_show = False
@ -203,76 +107,44 @@ class AssetsDialog(QtWidgets.QDialog):
self.reset(False)
def reset(self, force=True):
"""Reset asset model."""
"""Reset widget."""
if not force and not self._soft_reset_enabled:
return
if self._soft_reset_enabled:
self._soft_reset_enabled = False
self._model.reset()
def name_is_valid(self, name):
"""Is asset name valid.
Args:
name(str): Asset name that should be checked.
"""
# Make sure we're reset
self.reset(False)
# Valid the name by model
return self._model.name_is_valid(name)
self._folders_widget.set_project_name(self._controller.project_name)
def _on_filter_change(self, text):
"""Trigger change of filter of assets."""
self._proxy_model.setFilterFixedString(text)
"""Trigger change of filter of folders."""
self._folders_widget.set_name_filter(text)
def _on_cancel_clicked(self):
self.done(0)
def _on_ok_clicked(self):
index = self._asset_view.currentIndex()
asset_name = None
if index.isValid():
asset_name = index.data(ASSET_PATH_ROLE)
self._selected_asset = asset_name
self._selected_folder_path = (
self._folders_widget.get_selected_folder_path()
)
self.done(1)
def set_selected_assets(self, asset_names):
"""Change preselected asset before showing the dialog.
def set_selected_folders(self, folder_paths):
"""Change preselected folder before showing the dialog.
This also resets model and clean filter.
"""
self.reset(False)
self._asset_view.collapseAll()
self._filter_input.setText("")
indexes = []
for asset_name in asset_names:
index = self._model.get_index_by_asset_name(asset_name)
if index.isValid():
indexes.append(index)
folder_id = None
for folder_path in folder_paths:
folder_id = self._controller.get_folder_id_from_path(folder_path)
if folder_id:
break
if folder_id:
self._folders_widget.set_selected_folder(folder_id)
if not indexes:
return
index_deque = collections.deque()
for index in indexes:
index_deque.append(index)
all_indexes = []
while index_deque:
index = index_deque.popleft()
all_indexes.append(index)
parent_index = index.parent()
if parent_index.isValid():
index_deque.append(parent_index)
for index in all_indexes:
proxy_index = self._proxy_model.mapFromSource(index)
self._asset_view.expand(proxy_index)
def get_selected_asset(self):
"""Get selected asset name."""
return self._selected_asset
def get_selected_folder_path(self):
"""Get selected folder path."""
return self._selected_folder_path

View file

@ -212,7 +212,7 @@ class CreateContextWidget(QtWidgets.QWidget):
def update_current_context_btn(self):
# Hide set current asset if there is no one
folder_path = self._controller.current_asset_name
folder_path = self._controller.current_folder_path
self._current_context_btn.setVisible(bool(folder_path))
def set_selected_context(self, folder_id, task_name):
@ -252,7 +252,7 @@ class CreateContextWidget(QtWidgets.QWidget):
folder_id = self._last_folder_id
task_name = self._last_selected_task_name
if folder_id is None:
folder_path = self._controller.current_asset_name
folder_path = self._controller.current_folder_path
folder_id = self._controller.get_folder_id_from_path(folder_path)
task_name = self._controller.current_task_name
self._hierarchy_controller.set_selected_project(
@ -273,7 +273,7 @@ class CreateContextWidget(QtWidgets.QWidget):
self.task_changed.emit()
def _on_current_context_click(self):
folder_path = self._controller.current_asset_name
folder_path = self._controller.current_folder_path
task_name = self._controller.current_task_name
folder_id = self._controller.get_folder_id_from_path(folder_path)
self._hierarchy_controller.set_expected_selection(

View file

@ -108,7 +108,7 @@ class CreateWidget(QtWidgets.QWidget):
self._controller = controller
self._asset_name = None
self._folder_path = None
self._product_names = None
self._selected_creator = None
@ -314,8 +314,8 @@ class CreateWidget(QtWidgets.QWidget):
self._use_current_context = True
@property
def current_asset_name(self):
return self._controller.current_asset_name
def current_folder_path(self):
return self._controller.current_folder_path
@property
def current_task_name(self):
@ -324,13 +324,13 @@ class CreateWidget(QtWidgets.QWidget):
def _context_change_is_enabled(self):
return self._context_widget.is_enabled()
def _get_asset_name(self):
def _get_folder_path(self):
folder_path = None
if self._context_change_is_enabled():
folder_path = self._context_widget.get_selected_folder_path()
if folder_path is None:
folder_path = self.current_asset_name
folder_path = self.current_folder_path
return folder_path or None
def _get_folder_id(self):
@ -364,12 +364,12 @@ class CreateWidget(QtWidgets.QWidget):
self._use_current_context = True
def refresh(self):
current_folder_path = self._controller.current_asset_name
current_folder_path = self._controller.current_folder_path
current_task_name = self._controller.current_task_name
# Get context before refresh to keep selection of asset and
# task widgets
folder_path = self._get_asset_name()
folder_path = self._get_folder_path()
task_name = self._get_task_name()
# Replace by current context if last loaded context was
@ -427,7 +427,7 @@ class CreateWidget(QtWidgets.QWidget):
if (
self._context_change_is_enabled()
and self._get_asset_name() is None
and self._get_folder_path() is None
):
# QUESTION how to handle invalid asset?
prereq_available = False
@ -449,23 +449,25 @@ class CreateWidget(QtWidgets.QWidget):
self._on_variant_change()
def _refresh_product_name(self):
asset_name = self._get_asset_name()
folder_path = self._get_folder_path()
# Skip if asset did not change
if self._asset_name and self._asset_name == asset_name:
if self._folder_path and self._folder_path == folder_path:
return
# Make sure `_asset_name` and `_product_names` variables are reset
self._asset_name = asset_name
# Make sure `_folder_path` and `_product_names` variables are reset
self._folder_path = folder_path
self._product_names = None
if asset_name is None:
if folder_path is None:
return
product_names = self._controller.get_existing_product_names(asset_name)
product_names = self._controller.get_existing_product_names(
folder_path
)
self._product_names = product_names
if product_names is None:
self.product_name_input.setText("< Asset is not set >")
self.product_name_input.setText("< Folder is not set >")
def _refresh_creators(self):
# Refresh creators and add their product types to list
@ -664,13 +666,13 @@ class CreateWidget(QtWidgets.QWidget):
self.product_name_input.setText("< Valid variant >")
return
asset_name = self._get_asset_name()
folder_path = self._get_folder_path()
task_name = self._get_task_name()
creator_idenfier = self._selected_creator.identifier
# Calculate product name with Creator plugin
try:
product_name = self._controller.get_product_name(
creator_idenfier, variant_value, task_name, asset_name
creator_idenfier, variant_value, task_name, folder_path
)
except TaskNotSetError:
self._create_btn.setEnabled(False)
@ -777,11 +779,11 @@ class CreateWidget(QtWidgets.QWidget):
variant = self.variant_input.text()
# Care about product name only if context change is enabled
product_name = None
asset_name = None
folder_path = None
task_name = None
if self._context_change_is_enabled():
product_name = self.product_name_input.text()
asset_name = self._get_asset_name()
folder_path = self._get_folder_path()
task_name = self._get_task_name()
pre_create_data = self._pre_create_widget.current_value()
@ -793,7 +795,7 @@ class CreateWidget(QtWidgets.QWidget):
# Where to define these data?
# - what data show be stored?
instance_data = {
"folderPath": asset_name,
"folderPath": folder_path,
"task": task_name,
"variant": variant,
"productType": product_type

View file

@ -10,11 +10,11 @@ TASK_ORDER_ROLE = QtCore.Qt.UserRole + 3
class TasksModel(QtGui.QStandardItemModel):
"""Tasks model.
Task model must have set context of asset documents.
Task model must have set context of folder paths.
Items in model are based on 0-infinite asset documents. Always contain
an interserction of context asset tasks. When no assets are in context
them model is empty if 2 or more are in context assets that don't have
Items in model are based on 0-infinite folders. Always contain
an interserction of context folder tasks. When no folders are in context
them model is empty if 2 or more are in context folders that don't have
tasks with same names then model is empty too.
Args:
@ -27,21 +27,21 @@ class TasksModel(QtGui.QStandardItemModel):
self._allow_empty_task = allow_empty_task
self._controller = controller
self._items_by_name = {}
self._asset_names = []
self._task_names_by_asset_name = {}
self._folder_paths = []
self._task_names_by_folder_path = {}
def set_asset_names(self, asset_names):
def set_folder_paths(self, folder_paths):
"""Set assets context."""
self._asset_names = asset_names
self._folder_paths = folder_paths
self.reset()
@staticmethod
def get_intersection_of_tasks(task_names_by_asset_name):
def get_intersection_of_tasks(task_names_by_folder_path):
"""Calculate intersection of task names from passed data.
Example:
```
# Passed `task_names_by_asset_name`
# Passed `task_names_by_folder_path`
{
"asset_1": ["compositing", "animation"],
"asset_2": ["compositing", "editorial"]
@ -54,10 +54,10 @@ class TasksModel(QtGui.QStandardItemModel):
```
Args:
task_names_by_asset_name (dict): Task names in iterable by parent.
task_names_by_folder_path (dict): Task names in iterable by parent.
"""
tasks = None
for task_names in task_names_by_asset_name.values():
for task_names in task_names_by_folder_path.values():
if tasks is None:
tasks = set(task_names)
else:
@ -67,41 +67,43 @@ class TasksModel(QtGui.QStandardItemModel):
break
return tasks or set()
def is_task_name_valid(self, asset_name, task_name):
"""Is task name available for asset.
def is_task_name_valid(self, folder_path, task_name):
"""Is task name available for folder.
Args:
asset_name (str): Name of asset where should look for task.
task_name (str): Name of task which should be available in asset's
folder_path (str): Name of asset where should look for task.
task_name (str): Name of task which should be available in folder
tasks.
"""
if asset_name not in self._task_names_by_asset_name:
if folder_path not in self._task_names_by_folder_path:
return False
if self._allow_empty_task and not task_name:
return True
task_names = self._task_names_by_asset_name[asset_name]
task_names = self._task_names_by_folder_path[folder_path]
if task_name in task_names:
return True
return False
def reset(self):
"""Update model by current context."""
if not self._asset_names:
if not self._folder_paths:
self._items_by_name = {}
self._task_names_by_asset_name = {}
self._task_names_by_folder_path = {}
self.clear()
return
task_names_by_asset_name = (
self._controller.get_task_names_by_asset_names(self._asset_names)
task_names_by_folder_path = (
self._controller.get_task_names_by_folder_paths(
self._folder_paths
)
)
self._task_names_by_asset_name = task_names_by_asset_name
self._task_names_by_folder_path = task_names_by_folder_path
new_task_names = self.get_intersection_of_tasks(
task_names_by_asset_name
task_names_by_folder_path
)
if self._allow_empty_task:
new_task_names.add("")

View file

@ -481,7 +481,7 @@ class AssetsField(BaseClickableFrame):
if not result:
return
asset_name = self._dialog.get_selected_asset()
asset_name = self._dialog.get_selected_folder_path()
if asset_name is None:
return
@ -495,7 +495,7 @@ class AssetsField(BaseClickableFrame):
self.value_changed.emit()
def _mouse_release_callback(self):
self._dialog.set_selected_assets(self._selected_items)
self._dialog.set_selected_folders(self._selected_items)
self._dialog.open()
def set_multiselection_text(self, text):