Merge pull request #1556 from pypeclub/feature/project_manager_docstrings

Add docstrings to Project manager tool
This commit is contained in:
Jakub Trllo 2021-06-11 16:05:42 +02:00 committed by GitHub
commit 275ebdb246
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 433 additions and 11 deletions

View file

@ -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

View file

@ -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 + "]*$")

View file

@ -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)

View file

@ -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"])

View file

@ -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)