From 56153c1d6c4e0b85389f7c5ce8f8d2dbf6ab9459 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 May 2021 19:56:32 +0200 Subject: [PATCH 01/56] first commit of added docstrings and comments --- .../project_manager/constants.py | 9 ++ .../project_manager/delegates.py | 46 ++++++++ .../project_manager/project_manager/model.py | 110 +++++++++++++++++- 3 files changed, 161 insertions(+), 4 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/constants.py b/openpype/tools/project_manager/project_manager/constants.py index 6fb4b991ed..67dea79e59 100644 --- a/openpype/tools/project_manager/project_manager/constants.py +++ b/openpype/tools/project_manager/project_manager/constants.py @@ -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 + "]*$") diff --git a/openpype/tools/project_manager/project_manager/delegates.py b/openpype/tools/project_manager/project_manager/delegates.py index 51edff028f..0e8dd38e68 100644 --- a/openpype/tools/project_manager/project_manager/delegates.py +++ b/openpype/tools/project_manager/project_manager/delegates.py @@ -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. + + Exected that editor will be used only on AssetItem which is 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) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 5000729adf..026f6e0228 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -21,7 +21,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 @@ -31,6 +35,7 @@ class ProjectModel(QtGui.QStandardItemModel): super(ProjectModel, self).__init__(*args, **kwargs) def refresh(self): + """Reload projects.""" self.dbcon.Session["AVALON_PROJECT"] = None project_items = [] @@ -63,6 +68,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 @@ -78,6 +89,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"), @@ -93,6 +119,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", @@ -141,13 +169,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) @@ -157,25 +191,41 @@ 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 + # Clear all current content 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 @@ -185,7 +235,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 = [] @@ -214,6 +265,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") @@ -282,9 +334,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: @@ -292,9 +346,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 @@ -305,18 +365,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]) @@ -324,6 +390,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] @@ -333,6 +400,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 @@ -341,6 +409,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() @@ -354,7 +427,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() @@ -362,11 +441,31 @@ class HierarchyModel(QtCore.QAbstractItemModel): return self.index_from_item(row, column, parent_item) def index_for_item(self, item, column=0): + """Index for passet 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 @@ -377,15 +476,18 @@ class HierarchyModel(QtCore.QAbstractItemModel): return QtCore.QModelIndex() def add_new_asset(self, source_index): + """Method to create new asset item in hierarchy.""" item_id = source_index.data(IDENTIFIER_ROLE) item = self.items_by_id[item_id] 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: From f804a443e56552f836016dec8c59a5cbb9605d43 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 21 May 2021 01:29:54 +0200 Subject: [PATCH 02/56] few more docstrings and comments in project manager --- .../project_manager/project_manager/model.py | 218 +++++++++++++++++- 1 file changed, 217 insertions(+), 1 deletion(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 026f6e0228..ab4569947e 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -476,7 +476,12 @@ class HierarchyModel(QtCore.QAbstractItemModel): return QtCore.QModelIndex() def add_new_asset(self, source_index): - """Method to create new asset item in hierarchy.""" + """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] @@ -504,6 +509,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] @@ -519,6 +531,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 @@ -558,12 +582,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 on items with matching ids. + + Flag is also removed on all parents of passed children as it wouldn't + make sence 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: @@ -610,9 +647,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: @@ -1156,12 +1195,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 @@ -1196,6 +1255,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): @@ -1205,11 +1265,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: @@ -1219,6 +1287,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 @@ -1229,6 +1298,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 = Queue() to_process.put(project_item) @@ -1349,6 +1421,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() @@ -1376,6 +1456,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 @@ -1394,6 +1478,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 @@ -1521,6 +1606,7 @@ class BaseItem: class RootItem(BaseItem): + """Invisible root item used as base item for model.""" item_type = "root" def __init__(self, model): @@ -1535,6 +1621,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 = { @@ -1578,21 +1668,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"] @@ -1607,10 +1708,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 = { @@ -1693,34 +1801,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): @@ -1755,6 +1886,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 {} @@ -1791,6 +1938,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" @@ -1810,6 +1959,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"] @@ -1824,6 +1974,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 @@ -1853,6 +2004,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 @@ -1863,12 +2016,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" @@ -1916,6 +2072,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 @@ -1947,9 +2105,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: @@ -1976,18 +2147,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) @@ -1995,6 +2183,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 = { @@ -2023,10 +2221,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"): @@ -2034,6 +2234,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"] @@ -2048,9 +2249,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 @@ -2069,6 +2272,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) @@ -2084,6 +2293,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 "" @@ -2091,6 +2301,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: @@ -2103,6 +2314,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 @@ -2110,12 +2323,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" @@ -2126,6 +2341,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"]) From f7c8ec1c00741295ab18623a8a16991a32f6beb7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 21 May 2021 01:46:42 +0200 Subject: [PATCH 03/56] even more comments --- .../project_manager/project_manager/model.py | 41 +++++++++++++++---- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index ab4569947e..a045ecaf27 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -674,12 +674,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 @@ -712,6 +726,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 @@ -737,21 +753,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)) @@ -765,11 +789,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): @@ -782,6 +807,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) From 08bf7e5b7c6e3a244ed1dbfb5aa7e663b5db8933 Mon Sep 17 00:00:00 2001 From: jezscha Date: Tue, 8 Jun 2021 09:59:30 +0000 Subject: [PATCH 04/56] Create draft PR for #1658 From c4fb8fdbe1ea45ce0fcbed33faf25860e4a982c1 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 8 Jun 2021 12:36:50 +0200 Subject: [PATCH 05/56] Global: fix duplicity of plugin --- .../integrate_ftrack_component_overwrite.py | 21 ------------------- 1 file changed, 21 deletions(-) delete mode 100644 openpype/plugins/publish/integrate_ftrack_component_overwrite.py diff --git a/openpype/plugins/publish/integrate_ftrack_component_overwrite.py b/openpype/plugins/publish/integrate_ftrack_component_overwrite.py deleted file mode 100644 index 047fd8462c..0000000000 --- a/openpype/plugins/publish/integrate_ftrack_component_overwrite.py +++ /dev/null @@ -1,21 +0,0 @@ -import pyblish.api - - -class IntegrateFtrackComponentOverwrite(pyblish.api.InstancePlugin): - """ - Set `component_overwrite` to True on all instances `ftrackComponentsList` - """ - - order = pyblish.api.IntegratorOrder + 0.49 - label = 'Overwrite ftrack created versions' - families = ["clip"] - optional = True - active = False - - def process(self, instance): - component_list = instance.data['ftrackComponentsList'] - - for cl in component_list: - cl['component_overwrite'] = True - self.log.debug('Component {} overwriting'.format( - cl['component_data']['name'])) From 90fb363ce069d85e153c483d15e47533b058a579 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 8 Jun 2021 15:56:30 +0200 Subject: [PATCH 06/56] allow to pass list of lists or dict to prepare_template_data --- openpype/lib/plugin_tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/lib/plugin_tools.py b/openpype/lib/plugin_tools.py index a5254af0da..b27830f048 100644 --- a/openpype/lib/plugin_tools.py +++ b/openpype/lib/plugin_tools.py @@ -91,7 +91,7 @@ def prepare_template_data(fill_pairs): """ fill_data = {} - for key, value in fill_pairs: + for key, value in dict(fill_pairs).items(): # Handle cases when value is `None` (standalone publisher) if value is None: continue From bdc009bf5b06dce9e83b429074418f755587bf8e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 8 Jun 2021 15:56:56 +0200 Subject: [PATCH 07/56] get_subset_name can accept dynamic_data --- openpype/lib/plugin_tools.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/openpype/lib/plugin_tools.py b/openpype/lib/plugin_tools.py index b27830f048..26f1a2f7c4 100644 --- a/openpype/lib/plugin_tools.py +++ b/openpype/lib/plugin_tools.py @@ -34,7 +34,8 @@ def get_subset_name( asset_id, project_name=None, host_name=None, - default_template=None + default_template=None, + dynamic_data=None ): if not family: return "" @@ -68,11 +69,16 @@ def get_subset_name( if not task_name and "{task" in template.lower(): raise TaskNotSetError() - fill_pairs = ( - ("variant", variant), - ("family", family), - ("task", task_name) - ) + fill_pairs = { + "variant": variant, + "family": family, + "task": task_name + } + if dynamic_data: + # Dynamic data may override default values + for key, value in dynamic_data.items(): + fill_pairs[key] = value + return template.format(**prepare_template_data(fill_pairs)) From 6553a19c65859561bf7695c4f796f5f8dc89f9bc Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 8 Jun 2021 15:59:50 +0200 Subject: [PATCH 08/56] creator may have defined dynamic subset keys for subset name filling --- openpype/plugin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/plugin.py b/openpype/plugin.py index 8ade0f3825..836d37ce70 100644 --- a/openpype/plugin.py +++ b/openpype/plugin.py @@ -16,6 +16,8 @@ class PypeCreatorMixin: Mixin class must be used as first in inheritance order to override methods. """ + dynamic_subset_keys = [] + @classmethod def get_subset_name( From c5972cd08f8ca25514c2ff0228551004e5a38099 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 8 Jun 2021 16:00:48 +0200 Subject: [PATCH 09/56] implemented creator method to get dynamic data of the creator --- openpype/plugin.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openpype/plugin.py b/openpype/plugin.py index 836d37ce70..e84d402844 100644 --- a/openpype/plugin.py +++ b/openpype/plugin.py @@ -18,6 +18,14 @@ class PypeCreatorMixin: """ dynamic_subset_keys = [] + @classmethod + def get_dynamic_data( + cls, variant, task_name, asset_id, project_name, host_name + ): + dynamic_data = {} + for key in cls.dynamic_subset_keys: + dynamic_data[key] = "{" + key + "}" + return dynamic_data @classmethod def get_subset_name( From 826d106f59b48a69f0738f2792edc27e55094f58 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 8 Jun 2021 16:01:10 +0200 Subject: [PATCH 10/56] creator's dynamic data are passed to get_subset_name --- openpype/plugin.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/openpype/plugin.py b/openpype/plugin.py index e84d402844..27a75daedf 100644 --- a/openpype/plugin.py +++ b/openpype/plugin.py @@ -31,8 +31,18 @@ class PypeCreatorMixin: def get_subset_name( cls, variant, task_name, asset_id, project_name, host_name=None ): + dynamic_data = cls.get_dynamic_data( + variant, task_name, asset_id, project_name, host_name + ) + return get_subset_name( - cls.family, variant, task_name, asset_id, project_name, host_name + cls.family, + variant, + task_name, + asset_id, + project_name, + host_name, + dynamic_data=dynamic_data ) From 602214bbcd3341fc06036c3011944a767f8f6763 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 8 Jun 2021 16:46:21 +0200 Subject: [PATCH 11/56] create render layer has dynamic data definitions --- .../plugins/create/create_render_layer.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render_layer.py b/openpype/hosts/tvpaint/plugins/create/create_render_layer.py index 585f0c87d7..82defceeb0 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render_layer.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render_layer.py @@ -11,6 +11,7 @@ class CreateRenderlayer(plugin.Creator): defaults = ["Main"] rename_group = True + render_pass = "beauty" subset_template = "{family}_{name}" rename_script_template = ( @@ -18,6 +19,24 @@ class CreateRenderlayer(plugin.Creator): " {clip_id} {group_id} {r} {g} {b} \"{name}\"" ) + dynamic_subset_keys = ["render_pass", "render_layer", "group"] + + @classmethod + def get_dynamic_data( + cls, variant, task_name, asset_id, project_name, host_name + ): + dynamic_data = super(CreateRenderlayer, cls).get_dynamic_data( + variant, task_name, asset_id, project_name, host_name + ) + # Use render pass name from creator's plugin + dynamic_data["render_pass"] = cls.render_pass + # Add variant to render layer + dynamic_data["render_layer"] = variant + # Change family for subset name fill + dynamic_data["family"] = "render" + + return dynamic_data + def process(self): self.log.debug("Query data from workfile.") instances = pipeline.list_instances() From c480fc837ada2258cfc3232ed9356081377a2cec Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 8 Jun 2021 16:46:39 +0200 Subject: [PATCH 12/56] create render pass has definition of dynamic keys --- .../tvpaint/plugins/create/create_render_pass.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render_pass.py b/openpype/hosts/tvpaint/plugins/create/create_render_pass.py index 09c68930f2..8c72a69d05 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render_pass.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render_pass.py @@ -15,6 +15,19 @@ class CreateRenderPass(plugin.Creator): defaults = ["Main"] subset_template = "{family}_{render_layer}_{pass}" + dynamic_subset_keys = ["render_pass", "render_layer", "layer"] + + @classmethod + def get_dynamic_data( + cls, variant, task_name, asset_id, project_name, host_name + ): + dynamic_data = super(CreateRenderPass, cls).get_dynamic_data( + variant, task_name, asset_id, project_name, host_name + ) + dynamic_data["render_pass"] = variant + dynamic_data["family"] = "render" + + return dynamic_data def process(self): self.log.debug("Query data from workfile.") From c698f1fd5537bb08e34b5b9b874f9bc0d80221a6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 8 Jun 2021 16:47:30 +0200 Subject: [PATCH 13/56] removed extraction of variant from subset name --- .../tvpaint/plugins/create/create_render_layer.py | 12 ------------ .../tvpaint/plugins/create/create_render_pass.py | 6 ------ 2 files changed, 18 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render_layer.py b/openpype/hosts/tvpaint/plugins/create/create_render_layer.py index 82defceeb0..65aa39f166 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render_layer.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render_layer.py @@ -67,18 +67,6 @@ class CreateRenderlayer(plugin.Creator): self.log.debug(f"Selected group id is \"{group_id}\".") self.data["group_id"] = group_id - family = self.data["family"] - # Extract entered name - name = self.data["subset"][len(family):] - self.log.info(f"Extracted name from subset name \"{name}\".") - self.data["name"] = name - - # Change subset name by template - subset_name = self.subset_template.format(**{ - "family": self.family, - "name": name - }) - self.log.info(f"New subset name \"{subset_name}\".") self.data["subset"] = subset_name # Check for instances of same group diff --git a/openpype/hosts/tvpaint/plugins/create/create_render_pass.py b/openpype/hosts/tvpaint/plugins/create/create_render_pass.py index 8c72a69d05..d9a1455cb7 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render_pass.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render_pass.py @@ -70,12 +70,6 @@ class CreateRenderPass(plugin.Creator): render_layer = beauty_instance["name"] - # Extract entered name - family = self.data["family"] - name = self.data["subset"] - # Is this right way how to get name? - name = name[len(family):] - self.log.info(f"Extracted name from subset name \"{name}\".") self.data["group_id"] = group_id self.data["pass"] = name From 2321e707712ca9ca5a42adc4f93226f6e7563e44 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 8 Jun 2021 16:49:20 +0200 Subject: [PATCH 14/56] use "variant" instead of previously extracted name --- .../hosts/tvpaint/plugins/create/create_render_layer.py | 2 +- .../hosts/tvpaint/plugins/create/create_render_pass.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render_layer.py b/openpype/hosts/tvpaint/plugins/create/create_render_layer.py index 65aa39f166..04bd341bab 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render_layer.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render_layer.py @@ -122,7 +122,7 @@ class CreateRenderlayer(plugin.Creator): # Rename TVPaint group (keep color same) # - groups can't contain spaces - new_group_name = name.replace(" ", "_") + new_group_name = self.data["variant"].replace(" ", "_") rename_script = self.rename_script_template.format( clip_id=selected_group["clip_id"], group_id=selected_group["group_id"], diff --git a/openpype/hosts/tvpaint/plugins/create/create_render_pass.py b/openpype/hosts/tvpaint/plugins/create/create_render_pass.py index d9a1455cb7..f2b86985ec 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render_pass.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render_pass.py @@ -71,8 +71,10 @@ class CreateRenderPass(plugin.Creator): render_layer = beauty_instance["name"] + variant = self.data["variant"] + self.data["group_id"] = group_id - self.data["pass"] = name + self.data["pass"] = variant self.data["render_layer"] = render_layer # Collect selected layer ids to be stored into instance @@ -95,7 +97,7 @@ class CreateRenderPass(plugin.Creator): if ( instance["family"] == family and instance["group_id"] == group_id - and instance["pass"] == name + and instance["pass"] == variant ): existing_instance = instance existing_instance_idx = idx @@ -104,7 +106,7 @@ class CreateRenderPass(plugin.Creator): if existing_instance is not None: self.log.info( f"Render pass instance for group id {group_id}" - f" and name \"{name}\" already exists, overriding." + f" and name \"{variant}\" already exists, overriding." ) instances[existing_instance_idx] = self.data else: From f12f0694af42aae630ce3c645433a09a14b35b2a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 8 Jun 2021 16:50:02 +0200 Subject: [PATCH 15/56] set family back to creator's family --- openpype/hosts/tvpaint/plugins/create/create_render_layer.py | 4 ++++ openpype/hosts/tvpaint/plugins/create/create_render_pass.py | 3 +++ 2 files changed, 7 insertions(+) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render_layer.py b/openpype/hosts/tvpaint/plugins/create/create_render_layer.py index 04bd341bab..2a98fa54ac 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render_layer.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render_layer.py @@ -67,6 +67,10 @@ class CreateRenderlayer(plugin.Creator): self.log.debug(f"Selected group id is \"{group_id}\".") self.data["group_id"] = group_id + + # Set family back to "renderLayer" + family = self.family + self.data["family"] = family self.data["subset"] = subset_name # Check for instances of same group diff --git a/openpype/hosts/tvpaint/plugins/create/create_render_pass.py b/openpype/hosts/tvpaint/plugins/create/create_render_pass.py index f2b86985ec..4698c0b985 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render_pass.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render_pass.py @@ -70,6 +70,9 @@ class CreateRenderPass(plugin.Creator): render_layer = beauty_instance["name"] + # Set family back to "renderPass" + family = self.family + self.data["family"] = family variant = self.data["variant"] From e669de4ba1da5ffb30acf0122676f0e0071cd21c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 8 Jun 2021 16:51:18 +0200 Subject: [PATCH 16/56] replaced creation of new subset name with formatting dynamic keys in existing subset name --- .../plugins/create/create_render_pass.py | 40 ++++++++++++++----- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render_pass.py b/openpype/hosts/tvpaint/plugins/create/create_render_pass.py index 4698c0b985..da14326bc9 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render_pass.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render_pass.py @@ -1,5 +1,6 @@ from avalon.tvpaint import pipeline, lib from openpype.hosts.tvpaint.api import plugin +from openpype.lib import prepare_template_data class CreateRenderPass(plugin.Creator): @@ -68,7 +69,35 @@ class CreateRenderPass(plugin.Creator): if beauty_instance is None: raise AssertionError("Beauty pass does not exist yet.") - render_layer = beauty_instance["name"] + subset_name = self.data["subset"] + + subset_name_fill_data = {} + + layer_key = "{layer}" + if layer_key in subset_name.lower(): + if len(selected_layers) != 1: + raise CreatorError(( + "Subset name expect to use layer name but" + " multiple layers are selected." + )) + subset_name_fill_data["layer"] = selected_layers[0]["name"] + + # Backwards compatibility + # - beauty may be created with older creator where variant was not + # stored + if "variant" not in beauty_instance: + render_layer = beauty_instance["name"] + else: + render_layer = beauty_instance["variant"] + + subset_name_fill_data["render_layer"] = render_layer + + # Format dynamic keys in subset name + new_subset_name = subset_name.format( + **prepare_template_data(subset_name_fill_data) + ) + self.data["subset"] = new_subset_name + self.log.info(f"New subset name is \"{new_subset_name}\".") # Set family back to "renderPass" family = self.family @@ -84,15 +113,6 @@ class CreateRenderPass(plugin.Creator): layer_names = [layer["name"] for layer in selected_layers] self.data["layer_names"] = layer_names - # Replace `beauty` in beauty's subset name with entered name - subset_name = self.subset_template.format(**{ - "family": family, - "render_layer": render_layer, - "pass": name - }) - self.data["subset"] = subset_name - self.log.info(f"New subset name is \"{subset_name}\".") - # Check if same instance already exists existing_instance = None existing_instance_idx = None From 7b0126944bfd319d6bd95e38fa957092c2e16285 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 8 Jun 2021 16:52:12 +0200 Subject: [PATCH 17/56] added filling of dynamic keys in render layer creator --- .../plugins/create/create_render_layer.py | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render_layer.py b/openpype/hosts/tvpaint/plugins/create/create_render_layer.py index 2a98fa54ac..176bb52746 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render_layer.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render_layer.py @@ -1,5 +1,6 @@ from avalon.tvpaint import pipeline, lib from openpype.hosts.tvpaint.api import plugin +from openpype.lib import prepare_template_data class CreateRenderlayer(plugin.Creator): @@ -13,7 +14,6 @@ class CreateRenderlayer(plugin.Creator): rename_group = True render_pass = "beauty" - subset_template = "{family}_{name}" rename_script_template = ( "tv_layercolor \"setcolor\"" " {clip_id} {group_id} {r} {g} {b} \"{name}\"" @@ -67,10 +67,30 @@ class CreateRenderlayer(plugin.Creator): self.log.debug(f"Selected group id is \"{group_id}\".") self.data["group_id"] = group_id + group_data = lib.groups_data() + group_name = None + for group in group_data: + if group["group_id"] == group_id: + group_name = group["name"] + break + + if group_name is None: + raise AssertionError( + "Couldn't find group by id \"{}\"".format(group_id) + ) + + subset_name_fill_data = { + "group": group_name + } # Set family back to "renderLayer" family = self.family self.data["family"] = family + + # Fill dynamic key 'group' + subset_name = self.data["subset"].format( + **prepare_template_data(subset_name_fill_data) + ) self.data["subset"] = subset_name # Check for instances of same group From e003afadf9709320aac0630dee463ebbb704388e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 8 Jun 2021 16:52:30 +0200 Subject: [PATCH 18/56] use CreatorError for known issues on artist side --- .../hosts/tvpaint/plugins/create/create_render_layer.py | 7 ++++--- .../hosts/tvpaint/plugins/create/create_render_pass.py | 8 ++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render_layer.py b/openpype/hosts/tvpaint/plugins/create/create_render_layer.py index 176bb52746..75551bc69f 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render_layer.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render_layer.py @@ -1,3 +1,4 @@ +from avalon.api import CreatorError from avalon.tvpaint import pipeline, lib from openpype.hosts.tvpaint.api import plugin from openpype.lib import prepare_template_data @@ -51,16 +52,16 @@ class CreateRenderlayer(plugin.Creator): # Raise if there is no selection if not group_ids: - raise AssertionError("Nothing is selected.") + raise CreatorError("Nothing is selected.") # This creator should run only on one group if len(group_ids) > 1: - raise AssertionError("More than one group is in selection.") + raise CreatorError("More than one group is in selection.") group_id = tuple(group_ids)[0] # If group id is `0` it is `default` group which is invalid if group_id == 0: - raise AssertionError( + raise CreatorError( "Selection is not in group. Can't mark selection as Beauty." ) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render_pass.py b/openpype/hosts/tvpaint/plugins/create/create_render_pass.py index da14326bc9..f5792cc45b 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render_pass.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render_pass.py @@ -1,3 +1,4 @@ +from avalon.api import CreatorError from avalon.tvpaint import pipeline, lib from openpype.hosts.tvpaint.api import plugin from openpype.lib import prepare_template_data @@ -15,7 +16,6 @@ class CreateRenderPass(plugin.Creator): icon = "cube" defaults = ["Main"] - subset_template = "{family}_{render_layer}_{pass}" dynamic_subset_keys = ["render_pass", "render_layer", "layer"] @classmethod @@ -46,11 +46,11 @@ class CreateRenderPass(plugin.Creator): # Raise if nothing is selected if not selected_layers: - raise AssertionError("Nothing is selected.") + raise CreatorError("Nothing is selected.") # Raise if layers from multiple groups are selected if len(group_ids) != 1: - raise AssertionError("More than one group is in selection.") + raise CreatorError("More than one group is in selection.") group_id = tuple(group_ids)[0] self.log.debug(f"Selected group id is \"{group_id}\".") @@ -67,7 +67,7 @@ class CreateRenderPass(plugin.Creator): # Beauty is required for this creator so raise if was not found if beauty_instance is None: - raise AssertionError("Beauty pass does not exist yet.") + raise CreatorError("Beauty pass does not exist yet.") subset_name = self.data["subset"] From b912cd4ef0f05446d2f9bd32f776f0057d723fb8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 8 Jun 2021 16:53:08 +0200 Subject: [PATCH 19/56] collector won't change subset name but kept backwards compatibility by checking "variant" key in instance data --- .../plugins/publish/collect_instances.py | 55 +++++++++++-------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_instances.py b/openpype/hosts/tvpaint/plugins/publish/collect_instances.py index 9b11f9fe80..b7738b0ac0 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_instances.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_instances.py @@ -119,19 +119,23 @@ class CollectInstances(pyblish.api.ContextPlugin): name = instance_data["name"] # Change label subset_name = instance_data["subset"] - instance_data["label"] = "{}_Beauty".format(name) - # Change subset name - # Final family of an instance will be `render` - new_family = "render" - task_name = io.Session["AVALON_TASK"] - new_subset_name = "{}{}_{}_Beauty".format( - new_family, task_name.capitalize(), name - ) - instance_data["subset"] = new_subset_name - self.log.debug("Changed subset name \"{}\"->\"{}\"".format( - subset_name, new_subset_name - )) + # Backwards compatibility + # - subset names were not stored as final subset names during creation + if "variant" not in instance_data: + instance_data["label"] = "{}_Beauty".format(name) + + # Change subset name + # Final family of an instance will be `render` + new_family = "render" + task_name = io.Session["AVALON_TASK"] + new_subset_name = "{}{}_{}_Beauty".format( + new_family, task_name.capitalize(), name + ) + instance_data["subset"] = new_subset_name + self.log.debug("Changed subset name \"{}\"->\"{}\"".format( + subset_name, new_subset_name + )) # Get all layers for the layer layers_data = context.data["layersData"] @@ -165,18 +169,21 @@ class CollectInstances(pyblish.api.ContextPlugin): render_layer = instance_data["render_layer"] instance_data["label"] = "{}_{}".format(render_layer, pass_name) - # Change subset name - # Final family of an instance will be `render` - new_family = "render" - old_subset_name = instance_data["subset"] - task_name = io.Session["AVALON_TASK"] - new_subset_name = "{}{}_{}_{}".format( - new_family, task_name.capitalize(), render_layer, pass_name - ) - instance_data["subset"] = new_subset_name - self.log.debug("Changed subset name \"{}\"->\"{}\"".format( - old_subset_name, new_subset_name - )) + # Backwards compatibility + # - subset names were not stored as final subset names during creation + if "variant" not in instance_data: + # Change subset name + # Final family of an instance will be `render` + new_family = "render" + old_subset_name = instance_data["subset"] + task_name = io.Session["AVALON_TASK"] + new_subset_name = "{}{}_{}_{}".format( + new_family, task_name.capitalize(), render_layer, pass_name + ) + instance_data["subset"] = new_subset_name + self.log.debug("Changed subset name \"{}\"->\"{}\"".format( + old_subset_name, new_subset_name + )) layers_data = context.data["layersData"] layers_by_name = { From 0fea8b201c52bc02e5ee4b41d2c6c23e2c06b67b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 8 Jun 2021 18:06:25 +0200 Subject: [PATCH 20/56] Global: validator plugin editorial asset names --- .../publish/validate_editorial_asset_name.py | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 openpype/plugins/publish/validate_editorial_asset_name.py diff --git a/openpype/plugins/publish/validate_editorial_asset_name.py b/openpype/plugins/publish/validate_editorial_asset_name.py new file mode 100644 index 0000000000..6548b8ea89 --- /dev/null +++ b/openpype/plugins/publish/validate_editorial_asset_name.py @@ -0,0 +1,113 @@ +import pyblish.api +from avalon import io +from pprint import pformat + + +class ValidateEditorialAssetName(pyblish.api.ContextPlugin): + """ Validating if editorial's asset names are not already created in db. + + Checking variations of names with different size of caps or with or without underscores. + """ + + order = pyblish.api.ValidatorOrder + label = "Validate Asset Name" + + def process(self, context): + + asset_and_parents = self.get_parents(context) + + if not io.Session: + io.install() + + db_assets = list(io.find( + {"type": "asset"}, {"name": 1, "data.parents": 1})) + self.log.debug("__ db_assets: {}".format(db_assets)) + + project_entities = { + str(e["name"]): e["data"]["parents"] for e in db_assets} + + self.log.debug("__ project_entities: {}".format( + pformat(project_entities))) + + assets_missing_name = {} + assets_wrong_parent = {} + for asset in asset_and_parents.keys(): + if asset not in project_entities.keys(): + # add to some nonexistent list for next layer of check + assets_missing_name.update({asset: asset_and_parents[asset]}) + continue + + if asset_and_parents[asset] != project_entities[asset]: + # add to some nonexistent list for next layer of check + assets_wrong_parent.update({ + asset: { + "required": asset_and_parents[asset], + "already_in_db": project_entities[asset] + } + }) + continue + + self.log.info("correct asset: {}".format(asset)) + + if assets_missing_name: + wrong_names = {} + self.log.debug( + ">> assets_missing_name: {}".format(assets_missing_name)) + for asset in assets_missing_name.keys(): + _asset = asset.lower().replace("_", "") + if _asset in [a.lower().replace("_", "") + for a in project_entities.keys()]: + wrong_names.update({ + "required_name": asset, + "used_variants_in_db": [ + a for a in project_entities.keys() + if a.lower().replace("_", "") == _asset + ] + }) + + if wrong_names: + self.log.debug( + ">> wrong_names: {}".format(wrong_names)) + raise Exception( + "Some already existing asset name variants `{}`".format( + wrong_names)) + + + + if assets_wrong_parent: + self.log.debug( + ">> assets_wrong_parent: {}".format(assets_wrong_parent)) + raise Exception( + "Wrong parents on assets `{}`".format(assets_wrong_parent)) + + def _get_all_assets(self, input_dict): + """ Returns asset names in list. + + List contains all asset names including parents + """ + for key in input_dict.keys(): + # check if child key is available + if input_dict[key].get("childs"): + # loop deeper + self._get_all_assets( + input_dict[key]["childs"]) + else: + self.all_testing_assets.append(key) + + def get_parents(self, context): + return_dict = {} + for instance in context: + asset = instance.data["asset"] + families = instance.data.get("families", []) + [ + instance.data["family"] + ] + # filter out non-shot families + if "shot" not in families: + continue + + parents = instance.data["parents"] + + return_dict.update({ + asset: [p["entity_name"] for p in parents] + }) + return return_dict From 7e164c8a470b4a1a7a91f153b00549443297b50f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 8 Jun 2021 18:11:35 +0200 Subject: [PATCH 21/56] hound: suggestions --- openpype/plugins/publish/validate_editorial_asset_name.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/plugins/publish/validate_editorial_asset_name.py b/openpype/plugins/publish/validate_editorial_asset_name.py index 6548b8ea89..60b8f76a07 100644 --- a/openpype/plugins/publish/validate_editorial_asset_name.py +++ b/openpype/plugins/publish/validate_editorial_asset_name.py @@ -6,7 +6,8 @@ from pprint import pformat class ValidateEditorialAssetName(pyblish.api.ContextPlugin): """ Validating if editorial's asset names are not already created in db. - Checking variations of names with different size of caps or with or without underscores. + Checking variations of names with different size of caps or with + or without underscores. """ order = pyblish.api.ValidatorOrder @@ -72,8 +73,6 @@ class ValidateEditorialAssetName(pyblish.api.ContextPlugin): "Some already existing asset name variants `{}`".format( wrong_names)) - - if assets_wrong_parent: self.log.debug( ">> assets_wrong_parent: {}".format(assets_wrong_parent)) From 7ae3cf157eb6714d00d83bde8591845afacaad22 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 8 Jun 2021 18:45:22 +0200 Subject: [PATCH 22/56] capitalization works only on char and number symbols --- openpype/lib/plugin_tools.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/openpype/lib/plugin_tools.py b/openpype/lib/plugin_tools.py index 26f1a2f7c4..1f2fb7a46e 100644 --- a/openpype/lib/plugin_tools.py +++ b/openpype/lib/plugin_tools.py @@ -97,6 +97,7 @@ def prepare_template_data(fill_pairs): """ fill_data = {} + regex = re.compile(r"[a-zA-Z0-9]") for key, value in dict(fill_pairs).items(): # Handle cases when value is `None` (standalone publisher) if value is None: @@ -108,13 +109,18 @@ def prepare_template_data(fill_pairs): # Capitalize only first char of value # - conditions are because of possible index errors + # - regex is to skip symbols that are not chars or numbers + # - e.g. "{key}" which starts with curly bracket capitalized = "" - if value: - # Upper first character - capitalized += value[0].upper() - # Append rest of string if there is any - if len(value) > 1: - capitalized += value[1:] + for idx in range(len(value or "")): + char = value[idx] + if not regex.match(char): + capitalized += char + else: + capitalized += char.upper() + capitalized += value[idx + 1:] + break + fill_data[key.capitalize()] = capitalized return fill_data From ef756634be163836cc97250047f3a473ba80def8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 9 Jun 2021 10:07:10 +0200 Subject: [PATCH 23/56] added default subset template for tvpaint render families to settings --- .../settings/defaults/project_settings/global.json | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 5f779fccfa..4351f18a60 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -215,6 +215,17 @@ "hosts": [], "tasks": [], "template": "{family}{Task}{Variant}" + }, + { + "families": [ + "renderLayer", + "renderPass" + ], + "hosts": [ + "tvpaint" + ], + "tasks": [], + "template": "{family}{Task}_{Render_layer}_{Render_pass}" } ] }, From fd350b2c8417d5a029a43fa55ccbfebc595feace Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 9 Jun 2021 10:32:16 +0200 Subject: [PATCH 24/56] process data contain right family --- openpype/hosts/tvpaint/plugins/create/create_render_layer.py | 4 +--- openpype/hosts/tvpaint/plugins/create/create_render_pass.py | 5 +---- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render_layer.py b/openpype/hosts/tvpaint/plugins/create/create_render_layer.py index 75551bc69f..cd0a86bda3 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render_layer.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render_layer.py @@ -84,9 +84,7 @@ class CreateRenderlayer(plugin.Creator): "group": group_name } - # Set family back to "renderLayer" - family = self.family - self.data["family"] = family + family = self.family = self.data["family"] # Fill dynamic key 'group' subset_name = self.data["subset"].format( diff --git a/openpype/hosts/tvpaint/plugins/create/create_render_pass.py b/openpype/hosts/tvpaint/plugins/create/create_render_pass.py index f5792cc45b..4ace19dff5 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render_pass.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render_pass.py @@ -99,10 +99,7 @@ class CreateRenderPass(plugin.Creator): self.data["subset"] = new_subset_name self.log.info(f"New subset name is \"{new_subset_name}\".") - # Set family back to "renderPass" - family = self.family - self.data["family"] = family - + family = self.data["family"] variant = self.data["variant"] self.data["group_id"] = group_id From 5e355657c3413397a1fa31e95352e58eb9600e4d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 9 Jun 2021 11:04:52 +0200 Subject: [PATCH 25/56] removed layer dynamic key from render pass --- .../tvpaint/plugins/create/create_render_pass.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render_pass.py b/openpype/hosts/tvpaint/plugins/create/create_render_pass.py index 4ace19dff5..ec04e9b2ee 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render_pass.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render_pass.py @@ -16,7 +16,7 @@ class CreateRenderPass(plugin.Creator): icon = "cube" defaults = ["Main"] - dynamic_subset_keys = ["render_pass", "render_layer", "layer"] + dynamic_subset_keys = ["render_pass", "render_layer"] @classmethod def get_dynamic_data( @@ -73,15 +73,6 @@ class CreateRenderPass(plugin.Creator): subset_name_fill_data = {} - layer_key = "{layer}" - if layer_key in subset_name.lower(): - if len(selected_layers) != 1: - raise CreatorError(( - "Subset name expect to use layer name but" - " multiple layers are selected." - )) - subset_name_fill_data["layer"] = selected_layers[0]["name"] - # Backwards compatibility # - beauty may be created with older creator where variant was not # stored From 0c34a12ae7bc1e1ac89b112316e45cee35aec8d1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 9 Jun 2021 12:07:27 +0200 Subject: [PATCH 26/56] added docstring for get_dynamic_data --- openpype/plugin.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/openpype/plugin.py b/openpype/plugin.py index 27a75daedf..45c9a08209 100644 --- a/openpype/plugin.py +++ b/openpype/plugin.py @@ -22,8 +22,34 @@ class PypeCreatorMixin: def get_dynamic_data( cls, variant, task_name, asset_id, project_name, host_name ): + """Return dynamic data for current Creator plugin. + + By default return keys from `dynamic_subset_keys` attribute as mapping + to keep formatted template unchanged. + + ``` + dynamic_subset_keys = ["my_key"] + --- + output = { + "my_key": "{my_key}" + } + ``` + + Dynamic keys may override default Creator keys (family, task, asset, + ...) but do it wisely if you need. + + All of keys will be converted into 3 variants unchanged, capitalized + and all upper letters. Because of that are all keys lowered. + + This method can be modified to prefill some values just keep in mind it + is class method. + + Returns: + dict: Fill data for subset name template. + """ dynamic_data = {} for key in cls.dynamic_subset_keys: + key = key.lower() dynamic_data[key] = "{" + key + "}" return dynamic_data From 311c1c21bf235ad0009ae41f439dde39c3ae2a91 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 9 Jun 2021 20:20:08 +0200 Subject: [PATCH 27/56] redirect stdout of ftrack process to devnull if sys.stdout is None --- .../modules/ftrack/ftrack_server/socket_thread.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/ftrack_server/socket_thread.py b/openpype/modules/ftrack/ftrack_server/socket_thread.py index fd407bb9f5..eb8ec4d06c 100644 --- a/openpype/modules/ftrack/ftrack_server/socket_thread.py +++ b/openpype/modules/ftrack/ftrack_server/socket_thread.py @@ -66,7 +66,16 @@ class SocketThread(threading.Thread): *self.additional_args, str(self.port) ) - self.subproc = subprocess.Popen(args, env=env, stdin=subprocess.PIPE) + kwargs = { + "env": env, + "stdin": subprocess.PIPE + } + if not sys.stdout: + # Redirect to devnull if stdout is None + kwargs["stdout"] = subprocess.DEVNULL + kwargs["stderr"] = subprocess.DEVNULL + + self.subproc = subprocess.Popen(args, **kwargs) # Listen for incoming connections sock.listen(1) From 0599e482719e0b5a57280efdb95bea36461c64e7 Mon Sep 17 00:00:00 2001 From: jezscha Date: Thu, 10 Jun 2021 12:28:21 +0000 Subject: [PATCH 28/56] Create draft PR for #1681 From baaf2ba967502ea3fe97e8d2c695dbdec9df143f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 10 Jun 2021 15:34:27 +0200 Subject: [PATCH 29/56] Hiero: checking obsolete versions of containers - also fix selection with trackitem name --- openpype/hosts/hiero/api/lib.py | 59 ++++++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/hiero/api/lib.py b/openpype/hosts/hiero/api/lib.py index d8a235be77..876fae5da9 100644 --- a/openpype/hosts/hiero/api/lib.py +++ b/openpype/hosts/hiero/api/lib.py @@ -190,7 +190,7 @@ def get_track_items( if not item.isEnabled(): continue if track_item_name: - if item.name() in track_item_name: + if track_item_name in item.name(): return item # make sure only track items with correct track names are added if track_name and track_name in track.name(): @@ -949,6 +949,54 @@ def sync_clip_name_to_data_asset(track_items_list): print("asset was changed in clip: {}".format(ti_name)) +def check_inventory_versions(): + """ + Actual version color idetifier of Loaded containers + + Check all track items and filter only + Loader nodes for its version. It will get all versions from database + and check if the node is having actual version. If not then it will color + it to red. + """ + from . import parse_container + from avalon import io + + # presets + clip_color_last = "green" + clip_color = "red" + + # get all track items from current timeline + for track_item in get_track_items(): + container = parse_container(track_item) + + if container: + # get representation from io + representation = io.find_one({ + "type": "representation", + "_id": io.ObjectId(container["representation"]) + }) + + # Get start frame from version data + version = io.find_one({ + "type": "version", + "_id": representation["parent"] + }) + + # get all versions in list + versions = io.find({ + "type": "version", + "parent": version["parent"] + }).distinct('name') + + max_version = max(versions) + + # set clip colour + if version.get("name") == max_version: + track_item.source().binItem().setColor(clip_color_last) + else: + track_item.source().binItem().setColor(clip_color) + + def selection_changed_timeline(event): """Callback on timeline to check if asset in data is the same as clip name. @@ -958,9 +1006,15 @@ def selection_changed_timeline(event): timeline_editor = event.sender selection = timeline_editor.selection() + selection = [ti for ti in selection + if isinstance(ti, hiero.core.TrackItem)] + # run checking function sync_clip_name_to_data_asset(selection) + # also mark old versions of loaded containers + check_inventory_versions() + def before_project_save(event): track_items = get_track_items( @@ -972,3 +1026,6 @@ def before_project_save(event): # run checking function sync_clip_name_to_data_asset(track_items) + + # also mark old versions of loaded containers + check_inventory_versions() From 7c6d609c701bc2b6dfe57da6118e23fb860c8879 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 10 Jun 2021 16:44:50 +0200 Subject: [PATCH 30/56] don't change label of render pass --- openpype/hosts/tvpaint/plugins/publish/collect_instances.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_instances.py b/openpype/hosts/tvpaint/plugins/publish/collect_instances.py index b7738b0ac0..898c056f31 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_instances.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_instances.py @@ -167,11 +167,11 @@ class CollectInstances(pyblish.api.ContextPlugin): ) # Change label render_layer = instance_data["render_layer"] - instance_data["label"] = "{}_{}".format(render_layer, pass_name) # Backwards compatibility # - subset names were not stored as final subset names during creation if "variant" not in instance_data: + instance_data["label"] = "{}_{}".format(render_layer, pass_name) # Change subset name # Final family of an instance will be `render` new_family = "render" From b4f8c2943425d34dcb3b3b8d05f82397bad939ea Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 10 Jun 2021 17:11:17 +0200 Subject: [PATCH 31/56] use get_subset_name to get subset name for workfile and review instances --- .../plugins/publish/collect_instances.py | 35 +++++++++++++++++-- .../plugins/publish/collect_workfile.py | 34 +++++++++++++++++- 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_instances.py b/openpype/hosts/tvpaint/plugins/publish/collect_instances.py index 898c056f31..4468bfae40 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_instances.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_instances.py @@ -4,6 +4,8 @@ import copy import pyblish.api from avalon import io +from openpype.lib import get_subset_name + class CollectInstances(pyblish.api.ContextPlugin): label = "Collect Instances" @@ -62,9 +64,38 @@ class CollectInstances(pyblish.api.ContextPlugin): # Different instance creation based on family instance = None if family == "review": - # Change subset name + # Change subset name of review instance + + # Collect asset doc to get asset id + # - not sure if it's good idea to require asset id in + # get_subset_name? + asset_name = context.data["workfile_context"]["asset"] + asset_doc = io.find_one( + { + "type": "asset", + "name": asset_name + }, + {"_id": 1} + ) + asset_id = None + if asset_doc: + asset_id = asset_doc["_id"] + + # Project name from workfile context + project_name = context.data["workfile_context"]["project"] + # Host name from environemnt variable + host_name = os.environ["AVALON_APP"] + # Use empty variant value + variant = "" task_name = io.Session["AVALON_TASK"] - new_subset_name = "{}{}".format(family, task_name.capitalize()) + new_subset_name = get_subset_name( + family, + variant, + task_name, + asset_id, + project_name, + host_name + ) instance_data["subset"] = new_subset_name instance = context.create_instance(**instance_data) diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_workfile.py b/openpype/hosts/tvpaint/plugins/publish/collect_workfile.py index b059be90bf..b61fec895f 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_workfile.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_workfile.py @@ -3,6 +3,8 @@ import json import pyblish.api from avalon import io +from openpype.lib import get_subset_name + class CollectWorkfile(pyblish.api.ContextPlugin): label = "Collect Workfile" @@ -20,8 +22,38 @@ class CollectWorkfile(pyblish.api.ContextPlugin): basename, ext = os.path.splitext(filename) instance = context.create_instance(name=basename) + # Get subset name of workfile instance + # Collect asset doc to get asset id + # - not sure if it's good idea to require asset id in + # get_subset_name? + family = "workfile" + asset_name = context.data["workfile_context"]["asset"] + asset_doc = io.find_one( + { + "type": "asset", + "name": asset_name + }, + {"_id": 1} + ) + asset_id = None + if asset_doc: + asset_id = asset_doc["_id"] + + # Project name from workfile context + project_name = context.data["workfile_context"]["project"] + # Host name from environemnt variable + host_name = os.environ["AVALON_APP"] + # Use empty variant value + variant = "" task_name = io.Session["AVALON_TASK"] - subset_name = "workfile" + task_name.capitalize() + subset_name = get_subset_name( + family, + variant, + task_name, + asset_id, + project_name, + host_name + ) # Create Workfile instance instance.data.update({ From 913f4819596056643d0ba86f3c303bb0f2d724f3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 10 Jun 2021 17:23:01 +0200 Subject: [PATCH 32/56] with windows shell prelaunch hook to found app hook and simplyfied it --- openpype/hooks/pre_foundry_apps.py | 27 +++++++++++++++ openpype/hooks/pre_with_windows_shell.py | 44 ------------------------ 2 files changed, 27 insertions(+), 44 deletions(-) create mode 100644 openpype/hooks/pre_foundry_apps.py delete mode 100644 openpype/hooks/pre_with_windows_shell.py diff --git a/openpype/hooks/pre_foundry_apps.py b/openpype/hooks/pre_foundry_apps.py new file mode 100644 index 0000000000..1b2c0c6eda --- /dev/null +++ b/openpype/hooks/pre_foundry_apps.py @@ -0,0 +1,27 @@ +import subprocess +from openpype.lib import PreLaunchHook + + +class LaunchWindowsShell(PreLaunchHook): + """Add shell command before executable. + + Some hosts have issues when are launched directly from python in that case + it is possible to prepend shell executable which will trigger process + instead. + """ + + # Should be as last hook because must change launch arguments to string + order = 1000 + app_groups = ["nuke", "nukex", "hiero", "nukestudio"] + platforms = ["windows"] + + def execute(self): + # Change `creationflags` to CREATE_NEW_CONSOLE + # - on Windows will nuke create new window using it's console + # Set `stdout` and `stderr` to None so new created console does not + # have redirected output to DEVNULL in build + self.launch_context.kwargs.update({ + "creationflags": subprocess.CREATE_NEW_CONSOLE, + "stdout": None, + "stderr": None + }) diff --git a/openpype/hooks/pre_with_windows_shell.py b/openpype/hooks/pre_with_windows_shell.py deleted file mode 100644 index 441ab1a675..0000000000 --- a/openpype/hooks/pre_with_windows_shell.py +++ /dev/null @@ -1,44 +0,0 @@ -import os -import subprocess -from openpype.lib import PreLaunchHook - - -class LaunchWithWindowsShell(PreLaunchHook): - """Add shell command before executable. - - Some hosts have issues when are launched directly from python in that case - it is possible to prepend shell executable which will trigger process - instead. - """ - - # Should be as last hook because must change launch arguments to string - order = 1000 - app_groups = ["nuke", "nukex", "hiero", "nukestudio"] - platforms = ["windows"] - - def execute(self): - launch_args = self.launch_context.clear_launch_args( - self.launch_context.launch_args) - new_args = [ - # Get comspec which is cmd.exe in most cases. - os.environ.get("COMSPEC", "cmd.exe"), - # NOTE change to "/k" if want to keep console opened - "/c", - # Convert arguments to command line arguments (as string) - "\"{}\"".format( - subprocess.list2cmdline(launch_args) - ) - ] - # Convert list to string - # WARNING this only works if is used as string - args_string = " ".join(new_args) - self.log.info(( - "Modified launch arguments to be launched with shell \"{}\"." - ).format(args_string)) - - # Replace launch args with new one - self.launch_context.launch_args = args_string - # Change `creationflags` to CREATE_NEW_CONSOLE - self.launch_context.kwargs["creationflags"] = ( - subprocess.CREATE_NEW_CONSOLE - ) From c88fb84184886516f190b3a91e22e9b6b6c68061 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 10 Jun 2021 17:32:14 +0200 Subject: [PATCH 33/56] changed class name and docstring --- openpype/hooks/pre_foundry_apps.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/openpype/hooks/pre_foundry_apps.py b/openpype/hooks/pre_foundry_apps.py index 1b2c0c6eda..85f68c6b60 100644 --- a/openpype/hooks/pre_foundry_apps.py +++ b/openpype/hooks/pre_foundry_apps.py @@ -2,12 +2,13 @@ import subprocess from openpype.lib import PreLaunchHook -class LaunchWindowsShell(PreLaunchHook): - """Add shell command before executable. +class LaunchFoundryAppsWindows(PreLaunchHook): + """Foundry applications have specific way how to launch them. - Some hosts have issues when are launched directly from python in that case - it is possible to prepend shell executable which will trigger process - instead. + Nuke is executed "like" python process so it is required to pass + `CREATE_NEW_CONSOLE` flag on windows to trigger creation of new console. + At the same time the newly created console won't create it's own stdout + and stderr handlers so they should not be redirected to DEVNULL. """ # Should be as last hook because must change launch arguments to string From c91a4637758c31e46d69e3bbc7cb4c23540feb92 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 10 Jun 2021 17:46:51 +0200 Subject: [PATCH 34/56] fixed typos --- openpype/tools/project_manager/project_manager/delegates.py | 2 +- openpype/tools/project_manager/project_manager/model.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/delegates.py b/openpype/tools/project_manager/project_manager/delegates.py index 0e8dd38e68..758ab079a9 100644 --- a/openpype/tools/project_manager/project_manager/delegates.py +++ b/openpype/tools/project_manager/project_manager/delegates.py @@ -172,7 +172,7 @@ class TypeDelegate(QtWidgets.QStyledItemDelegate): class ToolsDelegate(QtWidgets.QStyledItemDelegate): """Specific delegate for "tools_env" key. - Exected that editor will be used only on AssetItem which is only item that + Expected that editor will be used only on AssetItem which is only item that can have `tools_env` (except project). Delegate requires tools cache which is shared across all ToolsDelegate diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index a045ecaf27..8ec4fe627d 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -441,7 +441,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): return self.index_from_item(row, column, parent_item) def index_for_item(self, item, column=0): - """Index for passet item. + """Index for passed item. This is for cases that index operations are required on specific item. @@ -592,7 +592,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): """Remove deletion flag on items with matching ids. Flag is also removed on all parents of passed children as it wouldn't - make sence to not to do so. + make sense to not to do so. Children of passed item ids are by default also unset for deletion. From 30af1869b389dff522ad2ade95d89bb8fbc0a2d0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 10 Jun 2021 18:07:26 +0200 Subject: [PATCH 35/56] added basic info about the tool --- openpype/tools/project_manager/__init__.py | 21 +++++++++++++++++++ .../project_manager/project_manager/window.py | 2 ++ 2 files changed, 23 insertions(+) diff --git a/openpype/tools/project_manager/__init__.py b/openpype/tools/project_manager/__init__.py index 62fa8af8aa..1c5bfdcbd5 100644 --- a/openpype/tools/project_manager/__init__.py +++ b/openpype/tools/project_manager/__init__.py @@ -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 diff --git a/openpype/tools/project_manager/project_manager/window.py b/openpype/tools/project_manager/project_manager/window.py index a800214517..ea68b02bbd 100644 --- a/openpype/tools/project_manager/project_manager/window.py +++ b/openpype/tools/project_manager/project_manager/window.py @@ -16,6 +16,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) From 012139e389fdafb5193ee5bb9e75a58c2d712b44 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 10 Jun 2021 18:12:24 +0200 Subject: [PATCH 36/56] added `the` into docstrings --- openpype/tools/project_manager/project_manager/delegates.py | 4 ++-- openpype/tools/project_manager/project_manager/model.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/delegates.py b/openpype/tools/project_manager/project_manager/delegates.py index 758ab079a9..842352cba1 100644 --- a/openpype/tools/project_manager/project_manager/delegates.py +++ b/openpype/tools/project_manager/project_manager/delegates.py @@ -172,8 +172,8 @@ 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 only item that - can have `tools_env` (except project). + 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. diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 8ec4fe627d..76e673f920 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -591,7 +591,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): def remove_delete_flag(self, item_ids, with_children=True): """Remove deletion flag on items with matching ids. - Flag is also removed on all parents of passed children as it wouldn't + The flag is also removed on 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. From f95ea456b9009d93987da4c00fa51d791a5c06fa Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 10 Jun 2021 18:12:54 +0200 Subject: [PATCH 37/56] fix long line --- openpype/tools/project_manager/project_manager/model.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 76e673f920..bf8e450912 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -591,8 +591,8 @@ class HierarchyModel(QtCore.QAbstractItemModel): def remove_delete_flag(self, item_ids, with_children=True): """Remove deletion flag on items with matching ids. - The flag is also removed on all parents of passed children as it wouldn't - make sense to not to do so. + The flag is also removed on 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. From 008dc3b7d05454c647b8a1d12cb95e3e0a8822eb Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 10 Jun 2021 18:28:25 +0200 Subject: [PATCH 38/56] fixed on->from --- openpype/tools/project_manager/project_manager/model.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index bf8e450912..14c697dd5f 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -589,9 +589,9 @@ class HierarchyModel(QtCore.QAbstractItemModel): return None def remove_delete_flag(self, item_ids, with_children=True): - """Remove deletion flag on items with matching ids. + """Remove deletion flag from items with matching ids. - The flag is also removed on all parents of passed children as it + 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. From d0792a9b2cabc65deb5dc0dc106515e8bcb42a65 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 11 Jun 2021 07:42:51 +0000 Subject: [PATCH 39/56] Bump normalize-url from 4.5.0 to 4.5.1 in /website Bumps [normalize-url](https://github.com/sindresorhus/normalize-url) from 4.5.0 to 4.5.1. - [Release notes](https://github.com/sindresorhus/normalize-url/releases) - [Commits](https://github.com/sindresorhus/normalize-url/commits) --- updated-dependencies: - dependency-name: normalize-url dependency-type: indirect ... Signed-off-by: dependabot[bot] --- website/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/yarn.lock b/website/yarn.lock index f1527f5b76..2d5ec103d4 100644 --- a/website/yarn.lock +++ b/website/yarn.lock @@ -5834,9 +5834,9 @@ normalize-range@^0.1.2: integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI= normalize-url@^4.1.0, normalize-url@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.0.tgz#453354087e6ca96957bd8f5baf753f5982142129" - integrity sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ== + version "4.5.1" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a" + integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA== npm-run-path@^2.0.0: version "2.0.2" From 9ff545352a2560478c72ab801a0966a6f06317a2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 11 Jun 2021 10:11:34 +0200 Subject: [PATCH 40/56] Revert "with windows shell prelaunch hook to found app hook and simplyfied it" This reverts commit 913f4819596056643d0ba86f3c303bb0f2d724f3. # Conflicts: # openpype/hooks/pre_foundry_apps.py --- openpype/hooks/pre_foundry_apps.py | 28 --------------- openpype/hooks/pre_with_windows_shell.py | 44 ++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 28 deletions(-) delete mode 100644 openpype/hooks/pre_foundry_apps.py create mode 100644 openpype/hooks/pre_with_windows_shell.py diff --git a/openpype/hooks/pre_foundry_apps.py b/openpype/hooks/pre_foundry_apps.py deleted file mode 100644 index 85f68c6b60..0000000000 --- a/openpype/hooks/pre_foundry_apps.py +++ /dev/null @@ -1,28 +0,0 @@ -import subprocess -from openpype.lib import PreLaunchHook - - -class LaunchFoundryAppsWindows(PreLaunchHook): - """Foundry applications have specific way how to launch them. - - Nuke is executed "like" python process so it is required to pass - `CREATE_NEW_CONSOLE` flag on windows to trigger creation of new console. - At the same time the newly created console won't create it's own stdout - and stderr handlers so they should not be redirected to DEVNULL. - """ - - # Should be as last hook because must change launch arguments to string - order = 1000 - app_groups = ["nuke", "nukex", "hiero", "nukestudio"] - platforms = ["windows"] - - def execute(self): - # Change `creationflags` to CREATE_NEW_CONSOLE - # - on Windows will nuke create new window using it's console - # Set `stdout` and `stderr` to None so new created console does not - # have redirected output to DEVNULL in build - self.launch_context.kwargs.update({ - "creationflags": subprocess.CREATE_NEW_CONSOLE, - "stdout": None, - "stderr": None - }) diff --git a/openpype/hooks/pre_with_windows_shell.py b/openpype/hooks/pre_with_windows_shell.py new file mode 100644 index 0000000000..441ab1a675 --- /dev/null +++ b/openpype/hooks/pre_with_windows_shell.py @@ -0,0 +1,44 @@ +import os +import subprocess +from openpype.lib import PreLaunchHook + + +class LaunchWithWindowsShell(PreLaunchHook): + """Add shell command before executable. + + Some hosts have issues when are launched directly from python in that case + it is possible to prepend shell executable which will trigger process + instead. + """ + + # Should be as last hook because must change launch arguments to string + order = 1000 + app_groups = ["nuke", "nukex", "hiero", "nukestudio"] + platforms = ["windows"] + + def execute(self): + launch_args = self.launch_context.clear_launch_args( + self.launch_context.launch_args) + new_args = [ + # Get comspec which is cmd.exe in most cases. + os.environ.get("COMSPEC", "cmd.exe"), + # NOTE change to "/k" if want to keep console opened + "/c", + # Convert arguments to command line arguments (as string) + "\"{}\"".format( + subprocess.list2cmdline(launch_args) + ) + ] + # Convert list to string + # WARNING this only works if is used as string + args_string = " ".join(new_args) + self.log.info(( + "Modified launch arguments to be launched with shell \"{}\"." + ).format(args_string)) + + # Replace launch args with new one + self.launch_context.launch_args = args_string + # Change `creationflags` to CREATE_NEW_CONSOLE + self.launch_context.kwargs["creationflags"] = ( + subprocess.CREATE_NEW_CONSOLE + ) From 35192876dc3a4d643a45c7742456ce28de1bf538 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 11 Jun 2021 10:26:18 +0200 Subject: [PATCH 41/56] Global: suggested namespace change --- .../publish/validate_editorial_asset_name.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/openpype/plugins/publish/validate_editorial_asset_name.py b/openpype/plugins/publish/validate_editorial_asset_name.py index 60b8f76a07..ccea42dc37 100644 --- a/openpype/plugins/publish/validate_editorial_asset_name.py +++ b/openpype/plugins/publish/validate_editorial_asset_name.py @@ -24,26 +24,26 @@ class ValidateEditorialAssetName(pyblish.api.ContextPlugin): {"type": "asset"}, {"name": 1, "data.parents": 1})) self.log.debug("__ db_assets: {}".format(db_assets)) - project_entities = { + asset_db_docs = { str(e["name"]): e["data"]["parents"] for e in db_assets} self.log.debug("__ project_entities: {}".format( - pformat(project_entities))) + pformat(asset_db_docs))) assets_missing_name = {} assets_wrong_parent = {} for asset in asset_and_parents.keys(): - if asset not in project_entities.keys(): + if asset not in asset_db_docs.keys(): # add to some nonexistent list for next layer of check assets_missing_name.update({asset: asset_and_parents[asset]}) continue - if asset_and_parents[asset] != project_entities[asset]: + if asset_and_parents[asset] != asset_db_docs[asset]: # add to some nonexistent list for next layer of check assets_wrong_parent.update({ asset: { "required": asset_and_parents[asset], - "already_in_db": project_entities[asset] + "already_in_db": asset_db_docs[asset] } }) continue @@ -57,11 +57,11 @@ class ValidateEditorialAssetName(pyblish.api.ContextPlugin): for asset in assets_missing_name.keys(): _asset = asset.lower().replace("_", "") if _asset in [a.lower().replace("_", "") - for a in project_entities.keys()]: + for a in asset_db_docs.keys()]: wrong_names.update({ "required_name": asset, "used_variants_in_db": [ - a for a in project_entities.keys() + a for a in asset_db_docs.keys() if a.lower().replace("_", "") == _asset ] }) From e97aede56ac0d058bac3ed0210338d9470f61f61 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 11 Jun 2021 11:03:16 +0200 Subject: [PATCH 42/56] sort applications and tools alphabetically --- openpype/settings/entities/enum_entity.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index 5df365508c..758ceff77a 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -116,7 +116,7 @@ class AppsEnumEntity(BaseEnumEntity): system_settings_entity = self.get_entity_from_path("system_settings") valid_keys = set() - enum_items = [] + enum_items_list = [] applications_entity = system_settings_entity["applications"] for group_name, app_group in applications_entity.items(): enabled_entity = app_group.get("enabled") @@ -149,8 +149,12 @@ class AppsEnumEntity(BaseEnumEntity): full_label = variant_label full_name = "/".join((group_name, variant_name)) - enum_items.append({full_name: full_label}) + enum_items_list.append((full_name, full_label)) valid_keys.add(full_name) + + enum_items = [] + for key, value in sorted(enum_items_list, key=lambda item: item[0]): + enum_items.append({key: value}) return enum_items, valid_keys def set_override_state(self, *args, **kwargs): @@ -179,7 +183,7 @@ class ToolsEnumEntity(BaseEnumEntity): system_settings_entity = self.get_entity_from_path("system_settings") valid_keys = set() - enum_items = [] + enum_items_list = [] tool_groups_entity = system_settings_entity["tools"]["tool_groups"] for group_name, tool_group in tool_groups_entity.items(): # Try to get group label from entity @@ -204,8 +208,12 @@ class ToolsEnumEntity(BaseEnumEntity): else: tool_label = tool_name - enum_items.append({tool_name: tool_label}) + enum_items_list.append((tool_name, tool_label)) valid_keys.add(tool_name) + + enum_items = [] + for key, value in sorted(enum_items_list, key=lambda item: item[0]): + enum_items.append({key: value}) return enum_items, valid_keys def set_override_state(self, *args, **kwargs): From 2a7d99bb01edc7042fed97fecb265b95079246d8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 11 Jun 2021 11:13:05 +0200 Subject: [PATCH 43/56] sort them by label instead of name --- openpype/settings/entities/enum_entity.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index 758ceff77a..0b0575a255 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -153,7 +153,7 @@ class AppsEnumEntity(BaseEnumEntity): valid_keys.add(full_name) enum_items = [] - for key, value in sorted(enum_items_list, key=lambda item: item[0]): + for key, value in sorted(enum_items_list, key=lambda item: item[1]): enum_items.append({key: value}) return enum_items, valid_keys @@ -212,7 +212,7 @@ class ToolsEnumEntity(BaseEnumEntity): valid_keys.add(tool_name) enum_items = [] - for key, value in sorted(enum_items_list, key=lambda item: item[0]): + for key, value in sorted(enum_items_list, key=lambda item: item[1]): enum_items.append({key: value}) return enum_items, valid_keys From 5be6803ace63b4bec26f8c898bcf2fb3080e95c6 Mon Sep 17 00:00:00 2001 From: antirotor Date: Fri, 11 Jun 2021 09:53:33 +0000 Subject: [PATCH 44/56] Create draft PR for #1665 From 3addbe5ba6b746dfbbce18cd7c810d7cd6aeaab4 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 11 Jun 2021 12:30:41 +0200 Subject: [PATCH 45/56] handle invalid version in zip gracefully --- igniter/bootstrap_repos.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index 7c4f8b4b69..6eaea27116 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -972,8 +972,12 @@ class BootstrapRepos: "openpype/version.py") as version_file: zip_version = {} exec(version_file.read(), zip_version) - version_check = OpenPypeVersion( - version=zip_version["__version__"]) + try: + version_check = OpenPypeVersion( + version=zip_version["__version__"]) + except ValueError as e: + self._print(str(e), True) + return False version_main = version_check.get_main_version() # noqa: E501 detected_main = detected_version.get_main_version() # noqa: E501 From 4269f1b795bfa43d1d16ffabcef3e8fe5b0c3392 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 11 Jun 2021 14:29:03 +0200 Subject: [PATCH 46/56] Hiero: settings: added `mp4` to loader representations --- openpype/settings/defaults/project_settings/hiero.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/hiero.json b/openpype/settings/defaults/project_settings/hiero.json index 44b27fc16f..1dff3aac51 100644 --- a/openpype/settings/defaults/project_settings/hiero.json +++ b/openpype/settings/defaults/project_settings/hiero.json @@ -34,7 +34,8 @@ "jpeg", "png", "h264", - "mov" + "mov", + "mp4" ], "clip_name_template": "{asset}_{subset}_{representation}" } From 41313894ea8468e16c481b8edae268703ed89eaf Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 11 Jun 2021 18:41:32 +0200 Subject: [PATCH 47/56] timers manager use openpype style for message box --- openpype/modules/timers_manager/widget_user_idle.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/modules/timers_manager/widget_user_idle.py b/openpype/modules/timers_manager/widget_user_idle.py index 8b614f6a13..25b4e56650 100644 --- a/openpype/modules/timers_manager/widget_user_idle.py +++ b/openpype/modules/timers_manager/widget_user_idle.py @@ -1,6 +1,5 @@ -from avalon import style from Qt import QtCore, QtGui, QtWidgets -from openpype import resources +from openpype import resources, style class WidgetUserIdle(QtWidgets.QWidget): From ef776d9c0ec639b3369f33a247ad55ecc9c16082 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 11 Jun 2021 18:41:55 +0200 Subject: [PATCH 48/56] muster login widget is using openpype style --- openpype/modules/muster/widget_login.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/modules/muster/widget_login.py b/openpype/modules/muster/widget_login.py index d9af4cb99f..7c77857935 100644 --- a/openpype/modules/muster/widget_login.py +++ b/openpype/modules/muster/widget_login.py @@ -1,13 +1,12 @@ import os from Qt import QtCore, QtGui, QtWidgets -from avalon import style -from openpype import resources +from openpype import resources, style class MusterLogin(QtWidgets.QWidget): SIZE_W = 300 - SIZE_H = 130 + SIZE_H = 150 loginSignal = QtCore.Signal(object, object, object) From 923e8e8df95ee72b863659be608d4f29ed952a09 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 11 Jun 2021 18:42:22 +0200 Subject: [PATCH 49/56] buttons in muster login have with same width --- openpype/modules/muster/widget_login.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/openpype/modules/muster/widget_login.py b/openpype/modules/muster/widget_login.py index 7c77857935..231b52c6bd 100644 --- a/openpype/modules/muster/widget_login.py +++ b/openpype/modules/muster/widget_login.py @@ -122,7 +122,6 @@ class MusterLogin(QtWidgets.QWidget): super().keyPressEvent(key_event) def setError(self, msg): - self.error_label.setText(msg) self.error_label.show() @@ -148,6 +147,17 @@ class MusterLogin(QtWidgets.QWidget): def save_credentials(self, username, password): self.module.get_auth_token(username, password) + def showEvent(self, event): + super(MusterLogin, self).showEvent(event) + + # Make btns same width + max_width = max( + self.btn_ok.sizeHint().width(), + self.btn_cancel.sizeHint().width() + ) + self.btn_ok.setMinimumWidth(max_width) + self.btn_cancel.setMinimumWidth(max_width) + def closeEvent(self, event): event.ignore() self._close_widget() From 8049fcdace72c77f49a057d6c58100115f0f3962 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 11 Jun 2021 18:42:38 +0200 Subject: [PATCH 50/56] ftrack login use openpype style --- openpype/modules/ftrack/tray/login_dialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/tray/login_dialog.py b/openpype/modules/ftrack/tray/login_dialog.py index a6360a7380..6a5d0ea786 100644 --- a/openpype/modules/ftrack/tray/login_dialog.py +++ b/openpype/modules/ftrack/tray/login_dialog.py @@ -1,6 +1,6 @@ import os import requests -from avalon import style +from openpype import style from openpype.modules.ftrack.lib import credentials from . import login_tools from openpype import resources From e711af6118a08484e24800a290f1b06ab818cf73 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 11 Jun 2021 18:42:54 +0200 Subject: [PATCH 51/56] ftrack url is label instead of input (as user can't change it) --- openpype/modules/ftrack/tray/login_dialog.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/modules/ftrack/tray/login_dialog.py b/openpype/modules/ftrack/tray/login_dialog.py index 6a5d0ea786..cc5689bee5 100644 --- a/openpype/modules/ftrack/tray/login_dialog.py +++ b/openpype/modules/ftrack/tray/login_dialog.py @@ -46,8 +46,11 @@ class CredentialsDialog(QtWidgets.QDialog): self.user_label = QtWidgets.QLabel("Username:") self.api_label = QtWidgets.QLabel("API Key:") - self.ftsite_input = QtWidgets.QLineEdit() - self.ftsite_input.setReadOnly(True) + self.ftsite_input = QtWidgets.QLabel() + self.ftsite_input.setTextInteractionFlags( + QtCore.Qt.TextBrowserInteraction + ) + # self.ftsite_input.setReadOnly(True) self.ftsite_input.setCursor(QtGui.QCursor(QtCore.Qt.IBeamCursor)) self.user_input = QtWidgets.QLineEdit() From e23d56d7aa7fd70c71aa7071aa30704014ffa8b9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 11 Jun 2021 18:43:27 +0200 Subject: [PATCH 52/56] clockify widget is using openpype style --- openpype/modules/clockify/widgets.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/modules/clockify/widgets.py b/openpype/modules/clockify/widgets.py index 76f3a3f365..b51ea4df7f 100644 --- a/openpype/modules/clockify/widgets.py +++ b/openpype/modules/clockify/widgets.py @@ -1,6 +1,5 @@ from Qt import QtCore, QtGui, QtWidgets -from avalon import style -from openpype import resources +from openpype import resources, style class MessageWidget(QtWidgets.QWidget): From fbfb85c95d5575d99d14b14e56a6ca7abff65be1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 11 Jun 2021 18:43:42 +0200 Subject: [PATCH 53/56] simplified clockify widgets --- openpype/modules/clockify/widgets.py | 99 ++++++++++------------------ 1 file changed, 35 insertions(+), 64 deletions(-) diff --git a/openpype/modules/clockify/widgets.py b/openpype/modules/clockify/widgets.py index b51ea4df7f..e9f504b851 100644 --- a/openpype/modules/clockify/widgets.py +++ b/openpype/modules/clockify/widgets.py @@ -21,14 +21,6 @@ class MessageWidget(QtWidgets.QWidget): QtCore.Qt.WindowMinimizeButtonHint ) - # Font - self.font = QtGui.QFont() - self.font.setFamily("DejaVu Sans Condensed") - self.font.setPointSize(9) - self.font.setBold(True) - self.font.setWeight(50) - self.font.setKerning(True) - # Size setting self.resize(self.SIZE_W, self.SIZE_H) self.setMinimumSize(QtCore.QSize(self.SIZE_W, self.SIZE_H)) @@ -52,7 +44,6 @@ class MessageWidget(QtWidgets.QWidget): labels = [] for message in messages: label = QtWidgets.QLabel(message) - label.setFont(self.font) label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) label.setTextFormat(QtCore.Qt.RichText) label.setWordWrap(True) @@ -102,84 +93,64 @@ class ClockifySettings(QtWidgets.QWidget): icon = QtGui.QIcon(resources.pype_icon_filepath()) self.setWindowIcon(icon) + self.setWindowTitle("Clockify settings") self.setWindowFlags( QtCore.Qt.WindowCloseButtonHint | QtCore.Qt.WindowMinimizeButtonHint ) - self._translate = QtCore.QCoreApplication.translate - - # Font - self.font = QtGui.QFont() - self.font.setFamily("DejaVu Sans Condensed") - self.font.setPointSize(9) - self.font.setBold(True) - self.font.setWeight(50) - self.font.setKerning(True) - # Size setting self.resize(self.SIZE_W, self.SIZE_H) self.setMinimumSize(QtCore.QSize(self.SIZE_W, self.SIZE_H)) self.setMaximumSize(QtCore.QSize(self.SIZE_W+100, self.SIZE_H+100)) self.setStyleSheet(style.load_stylesheet()) - self.setLayout(self._main()) - self.setWindowTitle('Clockify settings') + self._ui_init() - def _main(self): - self.main = QtWidgets.QVBoxLayout() - self.main.setObjectName("main") + def _ui_init(self): + label_api_key = QtWidgets.QLabel("Clockify API key:") - self.form = QtWidgets.QFormLayout() - self.form.setContentsMargins(10, 15, 10, 5) - self.form.setObjectName("form") + input_api_key = QtWidgets.QLineEdit() + input_api_key.setFrame(True) + input_api_key.setPlaceholderText("e.g. XX1XxXX2x3x4xXxx") - self.label_api_key = QtWidgets.QLabel("Clockify API key:") - self.label_api_key.setFont(self.font) - self.label_api_key.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) - self.label_api_key.setTextFormat(QtCore.Qt.RichText) - self.label_api_key.setObjectName("label_api_key") + error_label = QtWidgets.QLabel("") + error_label.setTextFormat(QtCore.Qt.RichText) + error_label.setWordWrap(True) + error_label.hide() - self.input_api_key = QtWidgets.QLineEdit() - self.input_api_key.setEnabled(True) - self.input_api_key.setFrame(True) - self.input_api_key.setObjectName("input_api_key") - self.input_api_key.setPlaceholderText( - self._translate("main", "e.g. XX1XxXX2x3x4xXxx") - ) + form_layout = QtWidgets.QFormLayout() + form_layout.setContentsMargins(10, 15, 10, 5) + form_layout.addRow(label_api_key, input_api_key) + form_layout.addRow(error_label) - self.error_label = QtWidgets.QLabel("") - self.error_label.setFont(self.font) - self.error_label.setTextFormat(QtCore.Qt.RichText) - self.error_label.setObjectName("error_label") - self.error_label.setWordWrap(True) - self.error_label.hide() + btn_ok = QtWidgets.QPushButton("Ok") + btn_ok.setToolTip('Sets Clockify API Key so can Start/Stop timer') - self.form.addRow(self.label_api_key, self.input_api_key) - self.form.addRow(self.error_label) - - self.btn_group = QtWidgets.QHBoxLayout() - self.btn_group.addStretch(1) - self.btn_group.setObjectName("btn_group") - - self.btn_ok = QtWidgets.QPushButton("Ok") - self.btn_ok.setToolTip('Sets Clockify API Key so can Start/Stop timer') - self.btn_ok.clicked.connect(self.click_ok) - - self.btn_cancel = QtWidgets.QPushButton("Cancel") + btn_cancel = QtWidgets.QPushButton("Cancel") cancel_tooltip = 'Application won\'t start' if self.optional: cancel_tooltip = 'Close this window' - self.btn_cancel.setToolTip(cancel_tooltip) - self.btn_cancel.clicked.connect(self._close_widget) + btn_cancel.setToolTip(cancel_tooltip) - self.btn_group.addWidget(self.btn_ok) - self.btn_group.addWidget(self.btn_cancel) + btn_group = QtWidgets.QHBoxLayout() + btn_group.addStretch(1) + btn_group.addWidget(btn_ok) + btn_group.addWidget(btn_cancel) - self.main.addLayout(self.form) - self.main.addLayout(self.btn_group) + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.addLayout(form_layout) + main_layout.addLayout(btn_group) - return self.main + btn_ok.clicked.connect(self.click_ok) + btn_cancel.clicked.connect(self._close_widget) + + self.label_api_key = label_api_key + self.input_api_key = input_api_key + self.error_label = error_label + + self.btn_ok = btn_ok + self.btn_cancel = btn_cancel def setError(self, msg): self.error_label.setText(msg) From afb77da237a0a1c30317ed84edf0babb1cf3ea1f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 11 Jun 2021 18:43:57 +0200 Subject: [PATCH 54/56] buttons in clockify credentials widget has same width --- openpype/modules/clockify/widgets.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/openpype/modules/clockify/widgets.py b/openpype/modules/clockify/widgets.py index e9f504b851..fc8e7fa8a3 100644 --- a/openpype/modules/clockify/widgets.py +++ b/openpype/modules/clockify/widgets.py @@ -182,6 +182,17 @@ class ClockifySettings(QtWidgets.QWidget): "Entered invalid API key" ) + def showEvent(self, event): + super(ClockifySettings, self).showEvent(event) + + # Make btns same width + max_width = max( + self.btn_ok.sizeHint().width(), + self.btn_cancel.sizeHint().width() + ) + self.btn_ok.setMinimumWidth(max_width) + self.btn_cancel.setMinimumWidth(max_width) + def closeEvent(self, event): if self.optional is True: event.ignore() From d9259876f6965f88c7ec089ecaccae0288280b01 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 11 Jun 2021 18:44:04 +0200 Subject: [PATCH 55/56] fixed clockify api property --- openpype/modules/clockify/clockify_api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/modules/clockify/clockify_api.py b/openpype/modules/clockify/clockify_api.py index 3f0a9799b4..6af911fffc 100644 --- a/openpype/modules/clockify/clockify_api.py +++ b/openpype/modules/clockify/clockify_api.py @@ -36,6 +36,7 @@ class ClockifyAPI: self._secure_registry = None + @property def secure_registry(self): if self._secure_registry is None: self._secure_registry = OpenPypeSecureRegistry("clockify") From 3579f0bd4bada62b6c24f49d0878ab42cb55905c Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 12 Jun 2021 03:46:39 +0000 Subject: [PATCH 56/56] [Automated] Bump version --- CHANGELOG.md | 20 ++++++++++++++++++-- openpype/version.py | 2 +- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fae98ec11..537be94076 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,23 +1,39 @@ # Changelog -## [3.1.0-nightly.2](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.1.0-nightly.3](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.0.0...HEAD) #### 🚀 Enhancements +- Sort applications and tools alphabetically in Settings UI [\#1689](https://github.com/pypeclub/OpenPype/pull/1689) +- \#683 - Validate Frame Range in Standalone Publisher [\#1683](https://github.com/pypeclub/OpenPype/pull/1683) +- Hiero: old container versions identify with red color [\#1682](https://github.com/pypeclub/OpenPype/pull/1682) +- Project Manger: Default name column width [\#1669](https://github.com/pypeclub/OpenPype/pull/1669) +- Remove outline in stylesheet [\#1667](https://github.com/pypeclub/OpenPype/pull/1667) +- TVPaint: Creator take layer name as default value for subset variant [\#1663](https://github.com/pypeclub/OpenPype/pull/1663) +- TVPaint custom subset template [\#1662](https://github.com/pypeclub/OpenPype/pull/1662) +- Feature Slack integration [\#1657](https://github.com/pypeclub/OpenPype/pull/1657) - Nuke - Publish simplification [\#1653](https://github.com/pypeclub/OpenPype/pull/1653) +- StandalonePublisher: adding exception for adding `delete` tag to repre [\#1650](https://github.com/pypeclub/OpenPype/pull/1650) - \#1333 - added tooltip hints to Pyblish buttons [\#1649](https://github.com/pypeclub/OpenPype/pull/1649) #### 🐛 Bug fixes +- Bad zip can break OpenPype start [\#1691](https://github.com/pypeclub/OpenPype/pull/1691) +- Ftrack subprocess handle of stdout/stderr [\#1675](https://github.com/pypeclub/OpenPype/pull/1675) +- Settings list race condifiton and mutable dict list conversion [\#1671](https://github.com/pypeclub/OpenPype/pull/1671) - Mac launch arguments fix [\#1660](https://github.com/pypeclub/OpenPype/pull/1660) - Fix missing dbm python module [\#1652](https://github.com/pypeclub/OpenPype/pull/1652) -- Transparent branches in view on Mac [\#1648](https://github.com/pypeclub/OpenPype/pull/1648) - Add asset on task item [\#1646](https://github.com/pypeclub/OpenPype/pull/1646) - Project manager save and queue [\#1645](https://github.com/pypeclub/OpenPype/pull/1645) - New project anatomy values [\#1644](https://github.com/pypeclub/OpenPype/pull/1644) +**Merged pull requests:** + +- Bump normalize-url from 4.5.0 to 4.5.1 in /website [\#1686](https://github.com/pypeclub/OpenPype/pull/1686) +- Add docstrings to Project manager tool [\#1556](https://github.com/pypeclub/OpenPype/pull/1556) + # Changelog diff --git a/openpype/version.py b/openpype/version.py index d6d6a4544b..bf261d41b2 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.1.0-nightly.2" +__version__ = "3.1.0-nightly.3"