mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge pull request #1556 from pypeclub/feature/project_manager_docstrings
Add docstrings to Project manager tool
This commit is contained in:
commit
275ebdb246
5 changed files with 433 additions and 11 deletions
|
|
@ -1,3 +1,24 @@
|
|||
"""Project Manager tool
|
||||
|
||||
Purpose of the tool is to be able create and modify hierarchy under project
|
||||
ready for OpenPype pipeline usage. Tool also give ability to create new
|
||||
projects.
|
||||
|
||||
# Brief info
|
||||
Project hierarchy consist of two types "asset" and "task". Assets can be
|
||||
children of Project or other Asset. Task can be children of Asset.
|
||||
|
||||
It is not possible to have duplicated Asset name across whole project.
|
||||
It is not possible to have duplicated Task name under one Asset.
|
||||
|
||||
Asset can't be moved or renamed if has or it's children has published content.
|
||||
|
||||
Deleted assets are not deleted from database but their type is changed to
|
||||
"archived_asset".
|
||||
|
||||
Tool allows to modify Asset attributes like frame start/end, fps, etc.
|
||||
"""
|
||||
|
||||
from .project_manager import (
|
||||
ProjectManagerWindow,
|
||||
main
|
||||
|
|
|
|||
|
|
@ -2,12 +2,21 @@ import re
|
|||
from Qt import QtCore
|
||||
|
||||
|
||||
# Item identifier (unique ID - uuid4 is used)
|
||||
IDENTIFIER_ROLE = QtCore.Qt.UserRole + 1
|
||||
# Item has duplicated name (Asset and Task items)
|
||||
DUPLICATED_ROLE = QtCore.Qt.UserRole + 2
|
||||
# It is possible to move and rename items
|
||||
# - that is disabled if e.g. Asset has published content
|
||||
HIERARCHY_CHANGE_ABLE_ROLE = QtCore.Qt.UserRole + 3
|
||||
# Item is marked for deletion
|
||||
# - item will be deleted after hitting save
|
||||
REMOVED_ROLE = QtCore.Qt.UserRole + 4
|
||||
# Item type in string
|
||||
ITEM_TYPE_ROLE = QtCore.Qt.UserRole + 5
|
||||
# Item has opened editor (per column)
|
||||
EDITOR_OPENED_ROLE = QtCore.Qt.UserRole + 6
|
||||
|
||||
# Allowed symbols for any name
|
||||
NAME_ALLOWED_SYMBOLS = "a-zA-Z0-9_"
|
||||
NAME_REGEX = re.compile("^[" + NAME_ALLOWED_SYMBOLS + "]*$")
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@ from .multiselection_combobox import MultiSelectionComboBox
|
|||
|
||||
|
||||
class ResizeEditorDelegate(QtWidgets.QStyledItemDelegate):
|
||||
"""Implementation of private method from QStyledItemDelegate.
|
||||
|
||||
Force editor to resize into item size.
|
||||
"""
|
||||
@staticmethod
|
||||
def _q_smart_min_size(editor):
|
||||
min_size_hint = editor.minimumSizeHint()
|
||||
|
|
@ -67,6 +71,16 @@ class ResizeEditorDelegate(QtWidgets.QStyledItemDelegate):
|
|||
|
||||
|
||||
class NumberDelegate(QtWidgets.QStyledItemDelegate):
|
||||
"""Delegate for number attributes.
|
||||
|
||||
Editor correspond passed arguments.
|
||||
|
||||
Args:
|
||||
minimum(int, float): Minimum possible value.
|
||||
maximum(int, float): Maximum possible value.
|
||||
decimals(int): How many decimal points can be used. Float will be used
|
||||
as value if is higher than 0.
|
||||
"""
|
||||
def __init__(self, minimum, maximum, decimals, *args, **kwargs):
|
||||
super(NumberDelegate, self).__init__(*args, **kwargs)
|
||||
self.minimum = minimum
|
||||
|
|
@ -80,10 +94,13 @@ class NumberDelegate(QtWidgets.QStyledItemDelegate):
|
|||
editor = QtWidgets.QSpinBox(parent)
|
||||
|
||||
editor.setObjectName("NumberEditor")
|
||||
# Set min/max
|
||||
editor.setMinimum(self.minimum)
|
||||
editor.setMaximum(self.maximum)
|
||||
# Hide spinbox buttons
|
||||
editor.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)
|
||||
|
||||
# Try to set value from item
|
||||
value = index.data(QtCore.Qt.EditRole)
|
||||
if value is not None:
|
||||
try:
|
||||
|
|
@ -98,6 +115,8 @@ class NumberDelegate(QtWidgets.QStyledItemDelegate):
|
|||
|
||||
|
||||
class NameDelegate(QtWidgets.QStyledItemDelegate):
|
||||
"""Specific delegate for "name" key."""
|
||||
|
||||
def createEditor(self, parent, option, index):
|
||||
editor = NameTextEdit(parent)
|
||||
editor.setObjectName("NameEditor")
|
||||
|
|
@ -108,11 +127,26 @@ class NameDelegate(QtWidgets.QStyledItemDelegate):
|
|||
|
||||
|
||||
class TypeDelegate(QtWidgets.QStyledItemDelegate):
|
||||
"""Specific delegate for "type" key.
|
||||
|
||||
It is expected that will be used only for TaskItem which has modifiable
|
||||
type. Type values are defined with cached project document.
|
||||
|
||||
Args:
|
||||
project_doc_cache(ProjectDocCache): Project cache shared across all
|
||||
delegates (kind of a struct pointer).
|
||||
"""
|
||||
|
||||
def __init__(self, project_doc_cache, *args, **kwargs):
|
||||
self._project_doc_cache = project_doc_cache
|
||||
super(TypeDelegate, self).__init__(*args, **kwargs)
|
||||
|
||||
def createEditor(self, parent, option, index):
|
||||
"""Editor is using filtrable combobox.
|
||||
|
||||
Editor should not be possible to create new items or set values that
|
||||
are not in this method.
|
||||
"""
|
||||
editor = FilterComboBox(parent)
|
||||
editor.setObjectName("TypeEditor")
|
||||
editor.style().polish(editor)
|
||||
|
|
@ -136,6 +170,18 @@ class TypeDelegate(QtWidgets.QStyledItemDelegate):
|
|||
|
||||
|
||||
class ToolsDelegate(QtWidgets.QStyledItemDelegate):
|
||||
"""Specific delegate for "tools_env" key.
|
||||
|
||||
Expected that editor will be used only on AssetItem which is the only item
|
||||
that can have `tools_env` (except project).
|
||||
|
||||
Delegate requires tools cache which is shared across all ToolsDelegate
|
||||
objects.
|
||||
|
||||
Args:
|
||||
tools_cache (ToolsCache): Possible values of tools.
|
||||
"""
|
||||
|
||||
def __init__(self, tools_cache, *args, **kwargs):
|
||||
self._tools_cache = tools_cache
|
||||
super(ToolsDelegate, self).__init__(*args, **kwargs)
|
||||
|
|
|
|||
|
|
@ -20,7 +20,11 @@ from Qt import QtCore, QtGui
|
|||
|
||||
|
||||
class ProjectModel(QtGui.QStandardItemModel):
|
||||
project_changed = QtCore.Signal()
|
||||
"""Load possible projects to modify from MongoDB.
|
||||
|
||||
Mongo collection must contain project document with "type" "project" and
|
||||
matching "name" value with name of collection.
|
||||
"""
|
||||
|
||||
def __init__(self, dbcon, *args, **kwargs):
|
||||
self.dbcon = dbcon
|
||||
|
|
@ -30,6 +34,7 @@ class ProjectModel(QtGui.QStandardItemModel):
|
|||
super(ProjectModel, self).__init__(*args, **kwargs)
|
||||
|
||||
def refresh(self):
|
||||
"""Reload projects."""
|
||||
self.dbcon.Session["AVALON_PROJECT"] = None
|
||||
|
||||
project_items = []
|
||||
|
|
@ -62,6 +67,12 @@ class ProjectModel(QtGui.QStandardItemModel):
|
|||
|
||||
|
||||
class HierarchySelectionModel(QtCore.QItemSelectionModel):
|
||||
"""Selection model with defined allowed multiselection columns.
|
||||
|
||||
This model allows to select multiple rows and enter one of their
|
||||
editors to edit value of all selected rows.
|
||||
"""
|
||||
|
||||
def __init__(self, multiselection_columns, *args, **kwargs):
|
||||
super(HierarchySelectionModel, self).__init__(*args, **kwargs)
|
||||
self.multiselection_columns = multiselection_columns
|
||||
|
|
@ -77,6 +88,21 @@ class HierarchySelectionModel(QtCore.QItemSelectionModel):
|
|||
|
||||
|
||||
class HierarchyModel(QtCore.QAbstractItemModel):
|
||||
"""Main model for hierarchy modification and value changes.
|
||||
|
||||
Main part of ProjectManager.
|
||||
|
||||
Model should be able to load existing entities, create new, handle their
|
||||
validations like name duplication and validate if is possible to save it's
|
||||
data.
|
||||
|
||||
Args:
|
||||
dbcon (AvalonMongoDB): Connection to MongoDB with set AVALON_PROJECT in
|
||||
it's Session to current project.
|
||||
"""
|
||||
|
||||
# Definition of all possible columns with their labels in default order
|
||||
# - order is important as column names are used as keys for column indexes
|
||||
_columns_def = [
|
||||
("name", "Name"),
|
||||
("type", "Type"),
|
||||
|
|
@ -92,6 +118,8 @@ class HierarchyModel(QtCore.QAbstractItemModel):
|
|||
("pixelAspect", "Pixel aspect"),
|
||||
("tools_env", "Tools")
|
||||
]
|
||||
# Columns allowing multiselection in edit mode
|
||||
# - gives ability to set all of keys below on multiple items at once
|
||||
multiselection_columns = {
|
||||
"frameStart",
|
||||
"frameEnd",
|
||||
|
|
@ -140,13 +168,19 @@ class HierarchyModel(QtCore.QAbstractItemModel):
|
|||
return self._items_by_id
|
||||
|
||||
def _reset_root_item(self):
|
||||
"""Removes all previous content related to model."""
|
||||
self._root_item = RootItem(self)
|
||||
|
||||
def refresh_project(self):
|
||||
"""Reload project data and discard unsaved changes."""
|
||||
self.set_project(self._current_project, True)
|
||||
|
||||
@property
|
||||
def project_item(self):
|
||||
"""Access to current project item.
|
||||
|
||||
Model can have 0-1 ProjectItems at once.
|
||||
"""
|
||||
output = None
|
||||
for row in range(self._root_item.rowCount()):
|
||||
item = self._root_item.child(row)
|
||||
|
|
@ -156,6 +190,14 @@ class HierarchyModel(QtCore.QAbstractItemModel):
|
|||
return output
|
||||
|
||||
def set_project(self, project_name, force=False):
|
||||
"""Change project and discard unsaved changes.
|
||||
|
||||
Args:
|
||||
project_name(str): New project name. Or None if just clearing
|
||||
content.
|
||||
force(bool): Force to change project even if project name is same
|
||||
as current project.
|
||||
"""
|
||||
if self._current_project == project_name and not force:
|
||||
return
|
||||
|
||||
|
|
@ -166,19 +208,26 @@ class HierarchyModel(QtCore.QAbstractItemModel):
|
|||
self.clear()
|
||||
|
||||
self._current_project = project_name
|
||||
|
||||
# Skip if project is None
|
||||
if not project_name:
|
||||
return
|
||||
|
||||
# Find project'd document
|
||||
project_doc = self.dbcon.database[project_name].find_one(
|
||||
{"type": "project"},
|
||||
ProjectItem.query_projection
|
||||
)
|
||||
# Skip if project document does not exist
|
||||
# - this shouldn't happen using only UI elements
|
||||
if not project_doc:
|
||||
return
|
||||
|
||||
# Create project item
|
||||
project_item = ProjectItem(project_doc)
|
||||
self.add_item(project_item)
|
||||
|
||||
# Query all assets of the project
|
||||
asset_docs = self.dbcon.database[project_name].find(
|
||||
{"type": "asset"},
|
||||
AssetItem.query_projection
|
||||
|
|
@ -188,7 +237,8 @@ class HierarchyModel(QtCore.QAbstractItemModel):
|
|||
for asset_doc in asset_docs
|
||||
}
|
||||
|
||||
# Prepare booleans if asset item can be modified (name or hierarchy)
|
||||
# Check if asset have published content and prepare booleans
|
||||
# if asset item can be modified (name and hierarchy change)
|
||||
# - the same must be applied to all it's parents
|
||||
asset_ids = list(asset_docs_by_id.keys())
|
||||
result = []
|
||||
|
|
@ -217,6 +267,7 @@ class HierarchyModel(QtCore.QAbstractItemModel):
|
|||
count = item["count"]
|
||||
asset_modifiable[asset_id] = count < 1
|
||||
|
||||
# Store assets by their visual parent to be able create their hierarchy
|
||||
asset_docs_by_parent_id = collections.defaultdict(list)
|
||||
for asset_doc in asset_docs_by_id.values():
|
||||
parent_id = asset_doc["data"].get("visualParent")
|
||||
|
|
@ -285,9 +336,11 @@ class HierarchyModel(QtCore.QAbstractItemModel):
|
|||
|
||||
self.add_items(task_items, asset_item)
|
||||
|
||||
# Emit that project was successfully changed
|
||||
self.project_changed.emit()
|
||||
|
||||
def rowCount(self, parent=None):
|
||||
"""Number of rows for passed parent."""
|
||||
if parent is None or not parent.isValid():
|
||||
parent_item = self._root_item
|
||||
else:
|
||||
|
|
@ -295,9 +348,15 @@ class HierarchyModel(QtCore.QAbstractItemModel):
|
|||
return parent_item.rowCount()
|
||||
|
||||
def columnCount(self, *args, **kwargs):
|
||||
"""Number of columns is static for this model."""
|
||||
return self.columns_len
|
||||
|
||||
def data(self, index, role):
|
||||
"""Access data for passed index and it's role.
|
||||
|
||||
Model is using principles implemented in BaseItem so converts passed
|
||||
index column into key and ask item to return value for passed role.
|
||||
"""
|
||||
if not index.isValid():
|
||||
return None
|
||||
|
||||
|
|
@ -308,18 +367,24 @@ class HierarchyModel(QtCore.QAbstractItemModel):
|
|||
return item.data(role, key)
|
||||
|
||||
def setData(self, index, value, role=QtCore.Qt.EditRole):
|
||||
"""Store data to passed index under role.
|
||||
|
||||
Pass values to corresponding item and behave by it's result.
|
||||
"""
|
||||
if not index.isValid():
|
||||
return False
|
||||
|
||||
item = index.internalPointer()
|
||||
column = index.column()
|
||||
key = self.columns[column]
|
||||
# Capture asset name changes for duplcated asset names validation.
|
||||
if (
|
||||
key == "name"
|
||||
and role in (QtCore.Qt.EditRole, QtCore.Qt.DisplayRole)
|
||||
):
|
||||
self._rename_asset(item, value)
|
||||
|
||||
# Pass values to item and by result emi dataChanged signal or not
|
||||
result = item.setData(value, role, key)
|
||||
if result:
|
||||
self.dataChanged.emit(index, index, [role])
|
||||
|
|
@ -327,6 +392,7 @@ class HierarchyModel(QtCore.QAbstractItemModel):
|
|||
return result
|
||||
|
||||
def headerData(self, section, orientation, role):
|
||||
"""Header labels."""
|
||||
if role == QtCore.Qt.DisplayRole:
|
||||
if section < self.columnCount():
|
||||
return self.column_labels[section]
|
||||
|
|
@ -336,6 +402,7 @@ class HierarchyModel(QtCore.QAbstractItemModel):
|
|||
)
|
||||
|
||||
def flags(self, index):
|
||||
"""Index flags are defined by corresponding item."""
|
||||
item = index.internalPointer()
|
||||
if item is None:
|
||||
return QtCore.Qt.NoItemFlags
|
||||
|
|
@ -344,6 +411,11 @@ class HierarchyModel(QtCore.QAbstractItemModel):
|
|||
return item.flags(key)
|
||||
|
||||
def parent(self, index=None):
|
||||
"""Parent for passed index as QModelIndex.
|
||||
|
||||
Args:
|
||||
index(QModelIndex): Parent index. Root item is used if not passed.
|
||||
"""
|
||||
if not index.isValid():
|
||||
return QtCore.QModelIndex()
|
||||
|
||||
|
|
@ -357,7 +429,13 @@ class HierarchyModel(QtCore.QAbstractItemModel):
|
|||
return self.createIndex(parent_item.row(), 0, parent_item)
|
||||
|
||||
def index(self, row, column, parent=None):
|
||||
"""Return index for row/column under parent"""
|
||||
"""Return index for row/column under parent.
|
||||
|
||||
Args:
|
||||
row(int): Row number.
|
||||
column(int): Column number.
|
||||
parent(QModelIndex): Parent index. Root item is used if not passed.
|
||||
"""
|
||||
parent_item = None
|
||||
if parent is not None and parent.isValid():
|
||||
parent_item = parent.internalPointer()
|
||||
|
|
@ -365,11 +443,31 @@ class HierarchyModel(QtCore.QAbstractItemModel):
|
|||
return self.index_from_item(row, column, parent_item)
|
||||
|
||||
def index_for_item(self, item, column=0):
|
||||
"""Index for passed item.
|
||||
|
||||
This is for cases that index operations are required on specific item.
|
||||
|
||||
Args:
|
||||
item(BaseItem): Item from model that will be converted to
|
||||
corresponding QModelIndex.
|
||||
column(int): Which column will be part of returned index. By
|
||||
default is used column 0.
|
||||
"""
|
||||
return self.index_from_item(
|
||||
item.row(), column, item.parent()
|
||||
)
|
||||
|
||||
def index_from_item(self, row, column, parent=None):
|
||||
"""Index for passed row, column and parent item.
|
||||
|
||||
Same implementation as `index` method but "parent" is one of
|
||||
BaseItem objects instead of QModelIndex.
|
||||
|
||||
Args:
|
||||
row(int): Row number.
|
||||
column(int): Column number.
|
||||
parent(BaseItem): Parent item. Root item is used if not passed.
|
||||
"""
|
||||
if parent is None:
|
||||
parent = self._root_item
|
||||
|
||||
|
|
@ -380,6 +478,12 @@ class HierarchyModel(QtCore.QAbstractItemModel):
|
|||
return QtCore.QModelIndex()
|
||||
|
||||
def add_new_asset(self, source_index):
|
||||
"""Create new asset item in hierarchy.
|
||||
|
||||
Args:
|
||||
source_index(QModelIndex): Parent under which new asset will be
|
||||
added.
|
||||
"""
|
||||
item_id = source_index.data(IDENTIFIER_ROLE)
|
||||
item = self.items_by_id[item_id]
|
||||
|
||||
|
|
@ -389,9 +493,11 @@ class HierarchyModel(QtCore.QAbstractItemModel):
|
|||
if isinstance(item, (RootItem, ProjectItem)):
|
||||
name = "ep"
|
||||
new_row = None
|
||||
else:
|
||||
elif isinstance(item, AssetItem):
|
||||
name = None
|
||||
new_row = item.rowCount()
|
||||
else:
|
||||
return
|
||||
|
||||
asset_data = {}
|
||||
if name:
|
||||
|
|
@ -408,6 +514,13 @@ class HierarchyModel(QtCore.QAbstractItemModel):
|
|||
return result
|
||||
|
||||
def add_new_task(self, parent_index):
|
||||
"""Create new TaskItem under passed parent index or it's parent.
|
||||
|
||||
Args:
|
||||
parent_index(QModelIndex): Index of parent AssetItem under which
|
||||
will be task added. If index represents TaskItem it's parent is
|
||||
used as parent.
|
||||
"""
|
||||
item_id = parent_index.data(IDENTIFIER_ROLE)
|
||||
item = self.items_by_id[item_id]
|
||||
|
||||
|
|
@ -423,6 +536,18 @@ class HierarchyModel(QtCore.QAbstractItemModel):
|
|||
return self.add_item(new_child, parent)
|
||||
|
||||
def add_items(self, items, parent=None, start_row=None):
|
||||
"""Add new items with definition of QAbstractItemModel.
|
||||
|
||||
Trigger `beginInsertRows` and `endInsertRows` to trigger proper
|
||||
callbacks in view or proxy model.
|
||||
|
||||
Args:
|
||||
items(list[BaseItem]): List of item that will be inserted in model.
|
||||
parent(RootItem, ProjectItem, AssetItem): Parent of items under
|
||||
which will be items added. Root item is used if not passed.
|
||||
start_row(int): Define to which row will be items added. Next
|
||||
available row of parent is used if not passed.
|
||||
"""
|
||||
if parent is None:
|
||||
parent = self._root_item
|
||||
|
||||
|
|
@ -462,12 +587,25 @@ class HierarchyModel(QtCore.QAbstractItemModel):
|
|||
return indexes
|
||||
|
||||
def add_item(self, item, parent=None, row=None):
|
||||
"""Add single item into model."""
|
||||
result = self.add_items([item], parent, row)
|
||||
if result:
|
||||
return result[0]
|
||||
return None
|
||||
|
||||
def remove_delete_flag(self, item_ids, with_children=True):
|
||||
"""Remove deletion flag from items with matching ids.
|
||||
|
||||
The flag is also removed from all parents of passed children as it
|
||||
wouldn't make sense to not to do so.
|
||||
|
||||
Children of passed item ids are by default also unset for deletion.
|
||||
|
||||
Args:
|
||||
list(uuid4): Ids of model items where remove flag should be unset.
|
||||
with_children(bool): Unset remove flag also on all children of
|
||||
passed items.
|
||||
"""
|
||||
items_by_id = {}
|
||||
for item_id in item_ids:
|
||||
if item_id in items_by_id:
|
||||
|
|
@ -514,9 +652,11 @@ class HierarchyModel(QtCore.QAbstractItemModel):
|
|||
self._validate_asset_duplicity(name)
|
||||
|
||||
def delete_index(self, index):
|
||||
"""Delete item of the index from model."""
|
||||
return self.delete_indexes([index])
|
||||
|
||||
def delete_indexes(self, indexes):
|
||||
"""Delete items from model."""
|
||||
items_by_id = {}
|
||||
processed_ids = set()
|
||||
for index in indexes:
|
||||
|
|
@ -539,12 +679,26 @@ class HierarchyModel(QtCore.QAbstractItemModel):
|
|||
self._remove_item(item)
|
||||
|
||||
def _remove_item(self, item):
|
||||
"""Remove item from model or mark item for deletion.
|
||||
|
||||
Deleted items are using definition of QAbstractItemModel which call
|
||||
`beginRemoveRows` and `endRemoveRows` to trigger proper view and proxy
|
||||
model callbacks.
|
||||
|
||||
Item is not just removed but is checked if can be removed from model or
|
||||
just mark it for deletion for save.
|
||||
|
||||
First of all will find all related children and based on their
|
||||
attributes define if can be removed.
|
||||
"""
|
||||
# Skip if item is already marked for deletion
|
||||
is_removed = item.data(REMOVED_ROLE)
|
||||
if is_removed:
|
||||
return
|
||||
|
||||
parent = item.parent()
|
||||
|
||||
# Find all descendants and store them by parent id
|
||||
all_descendants = collections.defaultdict(dict)
|
||||
all_descendants[parent.id][item.id] = item
|
||||
|
||||
|
|
@ -577,6 +731,8 @@ class HierarchyModel(QtCore.QAbstractItemModel):
|
|||
if isinstance(cur_item, AssetItem):
|
||||
self._rename_asset(cur_item, None)
|
||||
|
||||
# Process tasks as last because their logic is based on parent
|
||||
# - tasks may be processed before parent check all asset children
|
||||
for task_item in task_children:
|
||||
_fill_children(_all_descendants, task_item, cur_item)
|
||||
return remove_item
|
||||
|
|
@ -602,21 +758,29 @@ class HierarchyModel(QtCore.QAbstractItemModel):
|
|||
if not all_without_children:
|
||||
continue
|
||||
|
||||
parent_item = self._items_by_id[parent_id]
|
||||
# Row ranges of items to remove
|
||||
# - store tuples of row "start", "end" (can be the same)
|
||||
row_ranges = []
|
||||
# Predefine start, end variables
|
||||
start_row = end_row = None
|
||||
chilren_by_row = {}
|
||||
parent_item = self._items_by_id[parent_id]
|
||||
for row in range(parent_item.rowCount()):
|
||||
child_item = parent_item.child(row)
|
||||
child_id = child_item.id
|
||||
# Not sure if this can happend
|
||||
# TODO validate this line it seems dangerous as start/end
|
||||
# row is not changed
|
||||
if child_id not in children:
|
||||
continue
|
||||
|
||||
chilren_by_row[row] = child_item
|
||||
children.pop(child_item.id)
|
||||
|
||||
remove_item = child_item.data(REMOVED_ROLE)
|
||||
if not remove_item or not child_item.is_new:
|
||||
removed_mark = child_item.data(REMOVED_ROLE)
|
||||
if not removed_mark or not child_item.is_new:
|
||||
# Skip row sequence store child for later processing
|
||||
# and store current start/end row range
|
||||
modified_children.append(child_item)
|
||||
if end_row is not None:
|
||||
row_ranges.append((start_row, end_row))
|
||||
|
|
@ -630,11 +794,12 @@ class HierarchyModel(QtCore.QAbstractItemModel):
|
|||
if end_row is not None:
|
||||
row_ranges.append((start_row, end_row))
|
||||
|
||||
parent_index = None
|
||||
for start, end in row_ranges:
|
||||
if parent_index is None:
|
||||
parent_index = self.index_for_item(parent_item)
|
||||
if not row_ranges:
|
||||
continue
|
||||
|
||||
# Remove items from model
|
||||
parent_index = self.index_for_item(parent_item)
|
||||
for start, end in row_ranges:
|
||||
self.beginRemoveRows(parent_index, start, end)
|
||||
|
||||
for idx in range(start, end + 1):
|
||||
|
|
@ -647,6 +812,8 @@ class HierarchyModel(QtCore.QAbstractItemModel):
|
|||
|
||||
self.endRemoveRows()
|
||||
|
||||
# Trigger data change to repaint items
|
||||
# - `BackgroundRole` is random role without any specific reason
|
||||
for item in modified_children:
|
||||
s_index = self.index_for_item(item)
|
||||
e_index = self.index_for_item(item, column=self.columns_len - 1)
|
||||
|
|
@ -1060,12 +1227,32 @@ class HierarchyModel(QtCore.QAbstractItemModel):
|
|||
self.index_moved.emit(new_index)
|
||||
|
||||
def move_vertical(self, indexes, direction):
|
||||
"""Move item vertically in model to matching parent if possible.
|
||||
|
||||
If passed indexes contain items that has parent<->child relation at any
|
||||
hierarchy level only the top parent is actually moved.
|
||||
|
||||
Example (items marked with star are passed in `indexes`):
|
||||
- shots*
|
||||
- ep01
|
||||
- ep01_sh0010*
|
||||
- ep01_sh0020*
|
||||
In this case only `shots` item will be moved vertically and
|
||||
both "ep01_sh0010" "ep01_sh0020" will stay as children of "ep01".
|
||||
|
||||
Args:
|
||||
indexes(list[QModelIndex]): Indexes that should be moved
|
||||
vertically.
|
||||
direction(int): Which way will be moved -1 or 1 to determine.
|
||||
"""
|
||||
if not indexes:
|
||||
return
|
||||
|
||||
# Convert single index to list of indexes
|
||||
if isinstance(indexes, QtCore.QModelIndex):
|
||||
indexes = [indexes]
|
||||
|
||||
# Just process single index
|
||||
if len(indexes) == 1:
|
||||
self._move_vertical_single(indexes[0], direction)
|
||||
return
|
||||
|
|
@ -1100,6 +1287,7 @@ class HierarchyModel(QtCore.QAbstractItemModel):
|
|||
self._move_vertical_single(index, direction)
|
||||
|
||||
def child_removed(self, child):
|
||||
"""Callback for removed child."""
|
||||
self._items_by_id.pop(child.id, None)
|
||||
|
||||
def column_name(self, column):
|
||||
|
|
@ -1109,11 +1297,19 @@ class HierarchyModel(QtCore.QAbstractItemModel):
|
|||
return None
|
||||
|
||||
def clear(self):
|
||||
"""Reset model."""
|
||||
self.beginResetModel()
|
||||
self._reset_root_item()
|
||||
self.endResetModel()
|
||||
|
||||
def save(self):
|
||||
"""Save all changes from current project manager session.
|
||||
|
||||
Will create new asset documents, update existing and asset documents
|
||||
marked for deletion are removed from mongo if has published content or
|
||||
their type is changed to `archived_asset` to not loose their data.
|
||||
"""
|
||||
# Check if all items are valid before save
|
||||
all_valid = True
|
||||
for item in self._items_by_id.values():
|
||||
if not item.is_valid:
|
||||
|
|
@ -1123,6 +1319,7 @@ class HierarchyModel(QtCore.QAbstractItemModel):
|
|||
if not all_valid:
|
||||
return
|
||||
|
||||
# Check project item and do not save without it
|
||||
project_item = None
|
||||
for _project_item in self._root_item.children():
|
||||
project_item = _project_item
|
||||
|
|
@ -1133,6 +1330,9 @@ class HierarchyModel(QtCore.QAbstractItemModel):
|
|||
project_name = project_item.name
|
||||
project_col = self.dbcon.database[project_name]
|
||||
|
||||
# Process asset items per one hierarchical level.
|
||||
# - new assets are inserted per one parent
|
||||
# - update and delete data are stored and processed at once at the end
|
||||
to_process = collections.deque()
|
||||
to_process.append(project_item)
|
||||
|
||||
|
|
@ -1253,6 +1453,14 @@ class HierarchyModel(QtCore.QAbstractItemModel):
|
|||
|
||||
|
||||
class BaseItem:
|
||||
"""Base item for HierarchyModel.
|
||||
|
||||
Is not meant to be used as real item but as superclass for all items used
|
||||
in HierarchyModel.
|
||||
|
||||
TODO cleanup some attributes and methods related only to AssetItem and
|
||||
TaskItem.
|
||||
"""
|
||||
columns = []
|
||||
# Use `set` for faster result
|
||||
editable_columns = set()
|
||||
|
|
@ -1280,6 +1488,10 @@ class BaseItem:
|
|||
self._data[key] = value
|
||||
|
||||
def name_icon(self):
|
||||
"""Icon shown next to name.
|
||||
|
||||
Item must imlpement this method to change it.
|
||||
"""
|
||||
return None
|
||||
|
||||
@property
|
||||
|
|
@ -1298,6 +1510,7 @@ class BaseItem:
|
|||
self._children.insert(row, item)
|
||||
|
||||
def _get_global_data(self, role):
|
||||
"""Global data getter without column specification."""
|
||||
if role == ITEM_TYPE_ROLE:
|
||||
return self.item_type
|
||||
|
||||
|
|
@ -1425,6 +1638,7 @@ class BaseItem:
|
|||
|
||||
|
||||
class RootItem(BaseItem):
|
||||
"""Invisible root item used as base item for model."""
|
||||
item_type = "root"
|
||||
|
||||
def __init__(self, model):
|
||||
|
|
@ -1439,6 +1653,10 @@ class RootItem(BaseItem):
|
|||
|
||||
|
||||
class ProjectItem(BaseItem):
|
||||
"""Item representing project document in Mongo.
|
||||
|
||||
Item is used only to read it's data. It is not possible to modify them.
|
||||
"""
|
||||
item_type = "project"
|
||||
|
||||
columns = {
|
||||
|
|
@ -1482,21 +1700,32 @@ class ProjectItem(BaseItem):
|
|||
|
||||
@property
|
||||
def project_id(self):
|
||||
"""Project Mongo ID."""
|
||||
return self._mongo_id
|
||||
|
||||
@property
|
||||
def asset_id(self):
|
||||
"""Should not be implemented.
|
||||
|
||||
TODO Remove this method from ProjectItem.
|
||||
"""
|
||||
return None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Project name"""
|
||||
return self._data["name"]
|
||||
|
||||
def child_parents(self):
|
||||
"""Used by children AssetItems for filling `data.parents` key."""
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def data_from_doc(cls, project_doc):
|
||||
"""Convert document data into item data.
|
||||
|
||||
Project data are used as default value for it's children.
|
||||
"""
|
||||
data = {
|
||||
"name": project_doc["name"],
|
||||
"type": project_doc["type"]
|
||||
|
|
@ -1511,10 +1740,17 @@ class ProjectItem(BaseItem):
|
|||
return data
|
||||
|
||||
def flags(self, *args, **kwargs):
|
||||
"""Project is enabled and selectable."""
|
||||
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
|
||||
|
||||
|
||||
class AssetItem(BaseItem):
|
||||
"""Item represent asset document.
|
||||
|
||||
Item have ability to set all required and optional data for OpenPype
|
||||
workflow. Some of them are not modifiable in specific cases e.g. when asset
|
||||
has published content it is not possible to change it's name or parent.
|
||||
"""
|
||||
item_type = "asset"
|
||||
|
||||
columns = {
|
||||
|
|
@ -1597,34 +1833,57 @@ class AssetItem(BaseItem):
|
|||
|
||||
@property
|
||||
def project_id(self):
|
||||
"""Access to project "parent" id which is always set."""
|
||||
if self._project_id is None:
|
||||
self._project_id = self.parent().project_id
|
||||
return self._project_id
|
||||
|
||||
@property
|
||||
def asset_id(self):
|
||||
"""Property access to mongo id."""
|
||||
return self.mongo_id
|
||||
|
||||
@property
|
||||
def is_new(self):
|
||||
"""Item was created during current project manager session."""
|
||||
return self.asset_id is None
|
||||
|
||||
@property
|
||||
def is_valid(self):
|
||||
"""Item is invalid for saving."""
|
||||
if self._is_duplicated or not self._data["name"]:
|
||||
return False
|
||||
return True
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Asset name.
|
||||
|
||||
Returns:
|
||||
str: If name is set.
|
||||
None: If name is not yet set in that case is AssetItem marked as
|
||||
invalid.
|
||||
"""
|
||||
return self._data["name"]
|
||||
|
||||
def child_parents(self):
|
||||
"""Chilren AssetItem can use this method to get it's parent names.
|
||||
|
||||
This is used for `data.parents` key on document.
|
||||
"""
|
||||
parents = self.parent().child_parents()
|
||||
parents.append(self.name)
|
||||
return parents
|
||||
|
||||
def to_doc(self):
|
||||
"""Convert item to Mongo document matching asset schema.
|
||||
|
||||
Method does no validate if item is valid or children are valid.
|
||||
|
||||
Returns:
|
||||
dict: Document with all related data about asset item also
|
||||
contains task children.
|
||||
"""
|
||||
tasks = {}
|
||||
for item in self.children():
|
||||
if isinstance(item, TaskItem):
|
||||
|
|
@ -1659,6 +1918,22 @@ class AssetItem(BaseItem):
|
|||
return doc
|
||||
|
||||
def update_data(self):
|
||||
"""Changes dictionary ready for Mongo's update.
|
||||
|
||||
Method should be used on save. There is not other usage of this method.
|
||||
|
||||
# Example
|
||||
```python
|
||||
{
|
||||
"$set": {
|
||||
"name": "new_name"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Returns:
|
||||
dict: May be empty if item was not changed.
|
||||
"""
|
||||
if not self.mongo_id:
|
||||
return {}
|
||||
|
||||
|
|
@ -1695,6 +1970,8 @@ class AssetItem(BaseItem):
|
|||
|
||||
@classmethod
|
||||
def data_from_doc(cls, asset_doc):
|
||||
"""Convert asset document from Mongo to item data."""
|
||||
# Minimum required data for cases that it is new AssetItem withoud doc
|
||||
data = {
|
||||
"name": None,
|
||||
"type": "asset"
|
||||
|
|
@ -1714,6 +1991,7 @@ class AssetItem(BaseItem):
|
|||
return data
|
||||
|
||||
def name_icon(self):
|
||||
"""Icon shown next to name."""
|
||||
if self.__class__._name_icons is None:
|
||||
self.__class__._name_icons = ResourceCache.get_icons()["asset"]
|
||||
|
||||
|
|
@ -1728,6 +2006,7 @@ class AssetItem(BaseItem):
|
|||
return self.__class__._name_icons[icon_type]
|
||||
|
||||
def _get_global_data(self, role):
|
||||
"""Global data getter without column specification."""
|
||||
if role == HIERARCHY_CHANGE_ABLE_ROLE:
|
||||
return self._hierarchy_changes_enabled
|
||||
|
||||
|
|
@ -1757,6 +2036,8 @@ class AssetItem(BaseItem):
|
|||
return super(AssetItem, self).data(role, key)
|
||||
|
||||
def setData(self, value, role, key=None):
|
||||
# Store information that column has opened editor
|
||||
# - DisplayRole for the column will return empty string
|
||||
if role == EDITOR_OPENED_ROLE:
|
||||
if key not in self._edited_columns:
|
||||
return False
|
||||
|
|
@ -1767,12 +2048,15 @@ class AssetItem(BaseItem):
|
|||
self._removed = value
|
||||
return True
|
||||
|
||||
# This can be set only on project load (or save)
|
||||
if role == HIERARCHY_CHANGE_ABLE_ROLE:
|
||||
if self._hierarchy_changes_enabled == value:
|
||||
return False
|
||||
self._hierarchy_changes_enabled = value
|
||||
return True
|
||||
|
||||
# Do not allow to change name if item is marked to not be able do any
|
||||
# hierarchical changes.
|
||||
if (
|
||||
role == QtCore.Qt.EditRole
|
||||
and key == "name"
|
||||
|
|
@ -1820,6 +2104,8 @@ class AssetItem(BaseItem):
|
|||
_item.setData(False, DUPLICATED_ROLE)
|
||||
|
||||
def _rename_task(self, item):
|
||||
# Skip processing if item is marked for removing
|
||||
# - item is not in any of attributes below
|
||||
if item.data(REMOVED_ROLE):
|
||||
return
|
||||
|
||||
|
|
@ -1851,9 +2137,22 @@ class AssetItem(BaseItem):
|
|||
self._task_name_by_item_id[item_id] = new_name
|
||||
|
||||
def on_task_name_change(self, task_item):
|
||||
"""Method called from TaskItem children on name change.
|
||||
|
||||
Helps to handle duplicated task name validations.
|
||||
"""
|
||||
|
||||
self._rename_task(task_item)
|
||||
|
||||
def on_task_remove_state_change(self, task_item):
|
||||
"""Method called from children TaskItem to handle name duplications.
|
||||
|
||||
Method is called when TaskItem children is marked for deletion or
|
||||
deletion was reversed.
|
||||
|
||||
Name is removed/added to task item mapping attribute and removed/added
|
||||
to `_task_items_by_name` used for determination of duplicated tasks.
|
||||
"""
|
||||
is_removed = task_item.data(REMOVED_ROLE)
|
||||
item_id = task_item.data(IDENTIFIER_ROLE)
|
||||
if is_removed:
|
||||
|
|
@ -1880,18 +2179,35 @@ class AssetItem(BaseItem):
|
|||
_item.setData(True, DUPLICATED_ROLE)
|
||||
|
||||
def add_child(self, item, row=None):
|
||||
"""Add new children.
|
||||
|
||||
Args:
|
||||
item(AssetItem, TaskItem): New added item.
|
||||
row(int): Optionally can be passed on which row (index) should be
|
||||
children added.
|
||||
"""
|
||||
if item in self._children:
|
||||
return
|
||||
|
||||
super(AssetItem, self).add_child(item, row)
|
||||
|
||||
# Call inner method for checking task name duplications
|
||||
if isinstance(item, TaskItem):
|
||||
self._add_task(item)
|
||||
|
||||
def remove_child(self, item):
|
||||
"""Remove one of children from AssetItem children.
|
||||
|
||||
Skipped if item is not children of item.
|
||||
|
||||
Args:
|
||||
item(AssetItem, TaskItem): Child item.
|
||||
"""
|
||||
if item not in self._children:
|
||||
return
|
||||
|
||||
# Call inner method to remove task from registered task name
|
||||
# validations.
|
||||
if isinstance(item, TaskItem):
|
||||
self._remove_task(item)
|
||||
|
||||
|
|
@ -1899,6 +2215,16 @@ class AssetItem(BaseItem):
|
|||
|
||||
|
||||
class TaskItem(BaseItem):
|
||||
"""Item representing Task item on Asset document.
|
||||
|
||||
Always should be AssetItem children and never should have any other
|
||||
childrens.
|
||||
|
||||
It's name value should be validated with it's parent which only knows if
|
||||
has same name as other sibling under same parent.
|
||||
"""
|
||||
|
||||
# String representation of item
|
||||
item_type = "task"
|
||||
|
||||
columns = {
|
||||
|
|
@ -1927,10 +2253,12 @@ class TaskItem(BaseItem):
|
|||
|
||||
@property
|
||||
def is_new(self):
|
||||
"""Task was created during current project manager session."""
|
||||
return self._is_new
|
||||
|
||||
@property
|
||||
def is_valid(self):
|
||||
"""Task valid for saving."""
|
||||
if self._is_duplicated or not self._data["type"]:
|
||||
return False
|
||||
if not self.data(QtCore.Qt.EditRole, "name"):
|
||||
|
|
@ -1938,6 +2266,7 @@ class TaskItem(BaseItem):
|
|||
return True
|
||||
|
||||
def name_icon(self):
|
||||
"""Icon shown next to name."""
|
||||
if self.__class__._name_icons is None:
|
||||
self.__class__._name_icons = ResourceCache.get_icons()["task"]
|
||||
|
||||
|
|
@ -1952,9 +2281,11 @@ class TaskItem(BaseItem):
|
|||
return self.__class__._name_icons[icon_type]
|
||||
|
||||
def add_child(self, item, row=None):
|
||||
"""Reimplement `add_child` to avoid adding items under task."""
|
||||
raise AssertionError("BUG: Can't add children to Task")
|
||||
|
||||
def _get_global_data(self, role):
|
||||
"""Global data getter without column specification."""
|
||||
if role == REMOVED_ROLE:
|
||||
return self._removed
|
||||
|
||||
|
|
@ -1973,6 +2304,12 @@ class TaskItem(BaseItem):
|
|||
return super(TaskItem, self)._get_global_data(role)
|
||||
|
||||
def to_doc_data(self):
|
||||
"""Data for Asset document.
|
||||
|
||||
Returns:
|
||||
dict: May be empty if task is marked as removed or with single key
|
||||
dict with name as key and task data as value.
|
||||
"""
|
||||
if self._removed:
|
||||
return {}
|
||||
data = copy.deepcopy(self._data)
|
||||
|
|
@ -1988,6 +2325,7 @@ class TaskItem(BaseItem):
|
|||
return False
|
||||
return self._edited_columns[key]
|
||||
|
||||
# Return empty string if column is edited
|
||||
if role == QtCore.Qt.DisplayRole and self._edited_columns.get(key):
|
||||
return ""
|
||||
|
||||
|
|
@ -1995,6 +2333,7 @@ class TaskItem(BaseItem):
|
|||
if key == "type":
|
||||
return self._data["type"]
|
||||
|
||||
# Always require task type filled
|
||||
if key == "name":
|
||||
if not self._data["type"]:
|
||||
if role == QtCore.Qt.DisplayRole:
|
||||
|
|
@ -2007,6 +2346,8 @@ class TaskItem(BaseItem):
|
|||
return super(TaskItem, self).data(role, key)
|
||||
|
||||
def setData(self, value, role, key=None):
|
||||
# Store information that item on a column is edited
|
||||
# - DisplayRole will return empty string in that case
|
||||
if role == EDITOR_OPENED_ROLE:
|
||||
if key not in self._edited_columns:
|
||||
return False
|
||||
|
|
@ -2014,12 +2355,14 @@ class TaskItem(BaseItem):
|
|||
return True
|
||||
|
||||
if role == REMOVED_ROLE:
|
||||
# Skip value change if is same as already set value
|
||||
if value == self._removed:
|
||||
return False
|
||||
self._removed = value
|
||||
self.parent().on_task_remove_state_change(self)
|
||||
return True
|
||||
|
||||
# Convert empty string to None on EditRole
|
||||
if (
|
||||
role == QtCore.Qt.EditRole
|
||||
and key == "name"
|
||||
|
|
@ -2030,6 +2373,7 @@ class TaskItem(BaseItem):
|
|||
result = super(TaskItem, self).setData(value, role, key)
|
||||
|
||||
if role == QtCore.Qt.EditRole:
|
||||
# Trigger task name change of parent AssetItem
|
||||
if (
|
||||
key == "name"
|
||||
or (key == "type" and not self._data["name"])
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ from avalon.api import AvalonMongoDB
|
|||
|
||||
|
||||
class ProjectManagerWindow(QtWidgets.QWidget):
|
||||
"""Main widget of Project Manager tool."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(ProjectManagerWindow, self).__init__(parent)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue