From 791c724d97ca8e10850d2de53a3a30f4805ccfb8 Mon Sep 17 00:00:00 2001 From: Jacky Date: Wed, 22 Jun 2022 12:16:55 +0800 Subject: [PATCH 01/25] Add get_publish_path function, can get publish path without using representation --- avalon/api.py | 2 ++ avalon/pipeline.py | 68 ++++++++++++++++++++++++++-------------------- 2 files changed, 40 insertions(+), 30 deletions(-) diff --git a/avalon/api.py b/avalon/api.py index 33e4a3bed..b608ced82 100644 --- a/avalon/api.py +++ b/avalon/api.py @@ -42,6 +42,7 @@ get_representation_context, update_current_task, + get_publish_path, get_representation_path, loaders_from_representation, @@ -96,6 +97,7 @@ "get_representation_context", "update_current_task", + "get_publish_path", "get_representation_path", "loaders_from_representation", diff --git a/avalon/pipeline.py b/avalon/pipeline.py index 315256ccb..31346f793 100644 --- a/avalon/pipeline.py +++ b/avalon/pipeline.py @@ -1359,6 +1359,42 @@ def switch(container, representation): return new_loader.switch_form_loader(container, new_representation, loader) +def get_publish_path(representation_name, version, subset, asset, + root=None, project=None): + project = project or io.find_one({"name": Session["AVALON_PROJECT"], "type": "project"}) + try: + template = project["config"]["template"]["publish"] + except KeyError: + log.debug( + "No template in project %s, " + "likely a bug" % project["name"] + ) + return None + + project_root = project["data"].get("root") + root = root or project_root or registered_root() + + data = { + "root": root, + "project": project.get("name"), + "asset": asset.get("name"), + "silo": asset.get("silo"), + "subset": subset.get("name"), + "version": version.get("name"), + "representation": representation_name, + "user": Session.get("AVALON_USER", getpass.getuser()), + "app": Session.get("AVALON_APP", ""), + "task": version.get("data", {}).get("task") or Session.get("AVALON_TASK") + } + + try: + path = template.format(**data) + except KeyError as e: + log.debug("Template references unavailable data: %s" % e) + return None + + return os.path.normpath(path) + def get_representation_path(representation): """Get filename from representation document @@ -1406,37 +1442,9 @@ def path_from_config(): ) return None - try: - template = project["config"]["template"]["publish"] - except KeyError: - log.debug( - "No template in project %s, " - "likely a bug" % project["name"] - ) - return None - - # Cannot fail, required members only - data = { - "root": registered_root(), - "project": project["name"], - "asset": asset["name"], - "silo": asset.get("silo"), - "subset": subset["name"], - "version": version_["name"], - "representation": representation["name"], - "user": Session.get("AVALON_USER", getpass.getuser()), - "app": Session.get("AVALON_APP", ""), - "task": Session.get("AVALON_TASK", "") - } + repr_root = representation["data"].get("reprRoot") - try: - path = template.format(**data) - except KeyError as e: - log.debug("Template references unavailable data: %s" % e) - return None - - if os.path.exists(path): - return os.path.normpath(path) + return get_publish_path(representation["name"], version_, subset, asset, root=repr_root, project=project) def path_from_data(): if "path" not in representation["data"]: From c45b7727034f05c84480e0646eb01091f9c9a6c7 Mon Sep 17 00:00:00 2001 From: Jacky Date: Thu, 11 Aug 2022 11:40:21 +0800 Subject: [PATCH 02/25] Avalon support episode and sequence --- avalon/schema/asset-2.0.json | 8 + avalon/schema/asset-3.0.json | 8 + avalon/schema/episode-1.0.json | 55 ++++++ avalon/schema/episode-2.0.json | 53 +++++ avalon/schema/sequence-1.0.json | 59 ++++++ avalon/schema/sequence-2.0.json | 57 ++++++ avalon/tools/models.py | 16 +- avalon/tools/projectmanager/app.py | 152 ++++++++++++++- avalon/tools/projectmanager/dialogs.py | 260 ++++++++++++++++++++++++- avalon/tools/projectmanager/lib.py | 128 ++++++++++++ 10 files changed, 786 insertions(+), 10 deletions(-) create mode 100644 avalon/schema/episode-1.0.json create mode 100644 avalon/schema/episode-2.0.json create mode 100644 avalon/schema/sequence-1.0.json create mode 100644 avalon/schema/sequence-2.0.json diff --git a/avalon/schema/asset-2.0.json b/avalon/schema/asset-2.0.json index 8ea33feec..da7cab496 100644 --- a/avalon/schema/asset-2.0.json +++ b/avalon/schema/asset-2.0.json @@ -33,6 +33,14 @@ "description": "Unique identifier to parent document", "example": "592c33475f8c1b064c4d1696" }, + "episode": { + "description": "Unique identifier to episode document", + "example": "592c33475f8c1b064c4d1696" + }, + "sequence": { + "description": "Unique identifier to sequence document", + "example": "592c33475f8c1b064c4d1696" + }, "name": { "description": "Name of asset", "type": "string", diff --git a/avalon/schema/asset-3.0.json b/avalon/schema/asset-3.0.json index c021601ea..f7d590f1f 100644 --- a/avalon/schema/asset-3.0.json +++ b/avalon/schema/asset-3.0.json @@ -32,6 +32,14 @@ "description": "Unique identifier to parent document", "example": "592c33475f8c1b064c4d1696" }, + "episode": { + "description": "Unique identifier to episode document", + "example": "592c33475f8c1b064c4d1696" + }, + "sequence": { + "description": "Unique identifier to sequence document", + "example": "592c33475f8c1b064c4d1696" + }, "name": { "description": "Name of asset", "type": "string", diff --git a/avalon/schema/episode-1.0.json b/avalon/schema/episode-1.0.json new file mode 100644 index 000000000..62637e05b --- /dev/null +++ b/avalon/schema/episode-1.0.json @@ -0,0 +1,55 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + + "title": "avalon-core:episode-1.0", + "description": "A unit of data", + + "type": "object", + + "additionalProperties": true, + + "required": [ + "schema", + "type", + "name", + "silo", + "data" + ], + + "properties": { + "schema": { + "description": "Schema identifier for payload", + "type": "string", + "enum": ["avalon-core:episode-1.0"], + "example": "avalon-core:episode-1.0" + }, + "type": { + "description": "The type of document", + "type": "string", + "enum": ["episode"], + "example": "episode" + }, + "parent": { + "description": "Unique identifier to parent document", + "example": "592c33475f8c1b064c4d1696" + }, + "name": { + "description": "Name of episode", + "type": "string", + "pattern": "^[a-zA-Z0-9_.]*$", + "example": "Bruce" + }, + "silo": { + "description": "Group or container of episode", + "type": "string", + "example": "episodes" + }, + "data": { + "description": "Document metadata", + "type": "object", + "example": {"key": "value"} + } + }, + + "definitions": {} +} \ No newline at end of file diff --git a/avalon/schema/episode-2.0.json b/avalon/schema/episode-2.0.json new file mode 100644 index 000000000..d0c1122b8 --- /dev/null +++ b/avalon/schema/episode-2.0.json @@ -0,0 +1,53 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + + "title": "avalon-core:episode-2.0", + "description": "A unit of data", + + "type": "object", + + "additionalProperties": true, + + "required": [ + "schema", + "type", + "name", + "data" + ], + + "properties": { + "schema": { + "description": "Schema identifier for payload", + "type": "string", + "enum": ["avalon-core:episode-2.0"], + "example": "avalon-core:episode-2.0" + }, + "type": { + "description": "The type of document", + "type": "string", + "enum": ["episode"], + "example": "episode" + }, + "parent": { + "description": "Unique identifier to parent document", + "example": "592c33475f8c1b064c4d1696" + }, + "name": { + "description": "Name of episode", + "type": "string", + "pattern": "^[a-zA-Z0-9_.]*$", + "example": "Bruce" + }, + "silo": { + "description": "(Deprecated) Group or container of episode", + "example": "episodes" + }, + "data": { + "description": "Document metadata", + "type": "object", + "example": {"key": "value"} + } + }, + + "definitions": {} +} diff --git a/avalon/schema/sequence-1.0.json b/avalon/schema/sequence-1.0.json new file mode 100644 index 000000000..c08c1eab5 --- /dev/null +++ b/avalon/schema/sequence-1.0.json @@ -0,0 +1,59 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + + "title": "avalon-core:sequence-1.0", + "description": "A unit of data", + + "type": "object", + + "additionalProperties": true, + + "required": [ + "schema", + "type", + "name", + "silo", + "data" + ], + + "properties": { + "schema": { + "description": "Schema identifier for payload", + "type": "string", + "enum": ["avalon-core:sequence-1.0"], + "example": "avalon-core:sequence-1.0" + }, + "type": { + "description": "The type of document", + "type": "string", + "enum": ["sequence"], + "example": "sequence" + }, + "parent": { + "description": "Unique identifier to parent document", + "example": "592c33475f8c1b064c4d1696" + }, + "episode": { + "description": "Unique identifier to episode document", + "example": "592c33475f8c1b064c4d1696" + }, + "name": { + "description": "Name of sequence", + "type": "string", + "pattern": "^[a-zA-Z0-9_.]*$", + "example": "Bruce" + }, + "silo": { + "description": "Group or container of sequence", + "type": "string", + "example": "sequences" + }, + "data": { + "description": "Document metadata", + "type": "object", + "example": {"key": "value"} + } + }, + + "definitions": {} +} \ No newline at end of file diff --git a/avalon/schema/sequence-2.0.json b/avalon/schema/sequence-2.0.json new file mode 100644 index 000000000..6ed078961 --- /dev/null +++ b/avalon/schema/sequence-2.0.json @@ -0,0 +1,57 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + + "title": "avalon-core:sequence-2.0", + "description": "A unit of data", + + "type": "object", + + "additionalProperties": true, + + "required": [ + "schema", + "type", + "name", + "data" + ], + + "properties": { + "schema": { + "description": "Schema identifier for payload", + "type": "string", + "enum": ["avalon-core:sequence-2.0"], + "example": "avalon-core:sequence-2.0" + }, + "type": { + "description": "The type of document", + "type": "string", + "enum": ["sequence"], + "example": "sequence" + }, + "parent": { + "description": "Unique identifier to parent document", + "example": "592c33475f8c1b064c4d1696" + }, + "episode": { + "description": "Unique identifier to episode document", + "example": "592c33475f8c1b064c4d1696" + }, + "name": { + "description": "Name of sequence", + "type": "string", + "pattern": "^[a-zA-Z0-9_.]*$", + "example": "Bruce" + }, + "silo": { + "description": "(Deprecated) Group or container of sequence", + "example": "sequences" + }, + "data": { + "description": "Document metadata", + "type": "object", + "example": {"key": "value"} + } + }, + + "definitions": {} +} diff --git a/avalon/tools/models.py b/avalon/tools/models.py index ceb42b1d3..2d08c9b2b 100644 --- a/avalon/tools/models.py +++ b/avalon/tools/models.py @@ -416,12 +416,16 @@ def refresh(self): self.add_child(self.trash_item, parent=None) # Get all assets sorted by name + db_episodes = io.find({"type": "episode"}).sort("name", 1) + db_sequences = io.find({"type": "sequence"}).sort("name", 1) db_assets = io.find({"type": "asset"}).sort("name", 1) project_doc = io.find_one({"type": "project"}) silos = [] if lib.project_use_silo(project_doc): - for _silo in db_assets.distinct("silo"): + for _silo in sorted(list(set(db_episodes.distinct("silo") + + db_sequences.distinct("silo") + + db_assets.distinct("silo")))): _filter = {"type": "silo", "name": _silo} silo_data = io.find_one(_filter) if not silo_data: @@ -431,6 +435,16 @@ def refresh(self): # Group the assets by their visual parent's id assets_by_parent = collections.defaultdict(list) + for episode in db_episodes: + parent_id = episode.get("data", {}).get("visualParent") + if parent_id is None and silos is not None: + parent_id = episode.get("silo") + assets_by_parent[parent_id].append(episode) + for sequence in db_sequences: + parent_id = sequence.get("data", {}).get("visualParent") + if parent_id is None and silos is not None: + parent_id = sequence.get("silo") + assets_by_parent[parent_id].append(sequence) for asset in db_assets: parent_id = asset.get("data", {}).get("visualParent") if parent_id is None and silos is not None: diff --git a/avalon/tools/projectmanager/app.py b/avalon/tools/projectmanager/app.py index 500557055..61700eed7 100644 --- a/avalon/tools/projectmanager/app.py +++ b/avalon/tools/projectmanager/app.py @@ -8,7 +8,7 @@ from .. import lib as tools_lib from ..widgets import AssetWidget, TaskWidget -from .dialogs import TasksCreateDialog, AssetCreateDialog +from .dialogs import TasksCreateDialog, AssetCreateDialog, EpisodeCreateDialog, SequenceCreateDialog from project_api.utils import shotgun_io from project_api.shot.create import ShotCreator @@ -171,9 +171,15 @@ def __init__(self, is_silo_project=None, parent=None): assets_layout = QtWidgets.QVBoxLayout(assets_widgets) assets = AssetWidget() assets.view.setSelectionMode(assets.view.ExtendedSelection) + add_episode = QtWidgets.QPushButton("Add episode") + add_sequence = QtWidgets.QPushButton("Add sequence") add_asset = QtWidgets.QPushButton("Add asset") + add_layout = QtWidgets.QHBoxLayout() + add_layout.addWidget(add_episode) + add_layout.addWidget(add_sequence) + add_layout.addWidget(add_asset) assets_layout.addWidget(assets) - assets_layout.addWidget(add_asset) + assets_layout.addLayout(add_layout) # region Shotgrid snyc shotgrid_icon = QtGui.QIcon( @@ -278,6 +284,8 @@ def __init__(self, is_silo_project=None, parent=None): } # signals + add_episode.clicked.connect(self.on_add_episode) + add_sequence.clicked.connect(self.on_add_sequence) add_asset.clicked.connect(self.on_add_asset) add_task.clicked.connect(self.on_add_task) options_accept.clicked.connect(self.on_task_options_accepted) @@ -384,6 +392,129 @@ def sync_from_shotgrid(self, project_name, func): # endregion print('Sync done.') + def on_add_episode(self): + """Show add episode dialog""" + + # Get parent asset (active index in selection) + model = self.data["model"]["assets"] + parent = model.get_active_asset() + + if parent and parent["type"] == "silo": + silo = parent["name"] + else: + silo = parent.get("_document", {}).get("silo") if parent else None + + dialog = EpisodeCreateDialog( + is_silo_required=self.is_silo_project, parent=self + ) + + def _on_episode_created(data): + """Callback whenever episode gets created""" + self.echo("Added episode: {name}".format(**data)) + model.refresh() + + # Preserve focus on the dialog label field + # This is to allow quick continuing of typing a new episode name + # whenever the user created one; this way we can press the "ENTER" + # key to add an episode and continue typing for the next. + dialog.data["label"]["label"].setFocus() + + def _on_current_asset_changed(): + """Callback on current asset changed in item widget. + + Whenever the current index changes in the item widget we want to + update under which asset we're creating *to be created* asset. + + """ + + parent = model.get_active_asset() + if parent and parent["type"] == "silo": + _silo = parent["name"] + dialog.set_silo(_silo) + + # Set initial values + dialog.set_silo(silo) + + # Signals + model.current_changed.connect(_on_current_asset_changed) + dialog.episode_created.connect(_on_episode_created) + + dialog.show() + + def on_add_sequence(self): + """Show add sequence dialog""" + + # Get parent asset (active index in selection) + model = self.data["model"]["assets"] + parent = model.get_active_asset() + + if parent: + if parent["type"] == "silo": + silo = parent["name"] + else: + silo = parent.get("_document", {}).get("silo") + if parent["type"] == "episode": + episode_id = parent["_id"] + else: + episode_id = None + else: + episode_id = None + silo = None + + dialog = SequenceCreateDialog( + is_silo_required=self.is_silo_project, parent=self + ) + + def _on_sequence_created(data): + """Callback whenever sequence gets created""" + self.echo("Added sequence: {name}".format(**data)) + model.refresh() + + # Preserve focus on the dialog label field + # This is to allow quick continuing of typing a new sequence name + # whenever the user created one; this way we can press the "ENTER" + # key to add an sequence and continue typing for the next. + dialog.data["label"]["label"].setFocus() + + def _on_current_asset_changed(): + """Callback on current asset changed in item widget. + + Whenever the current index changes in the item widget we want to + update under which asset we're creating *to be created* asset. + + """ + + parent = model.get_active_asset() + if parent: + if parent["type"] == "silo": + _episode_id = None + _silo = parent["name"] + elif parent["type"] == "episode": + _episode_id = parent["_id"] + _silo = parent.get("_document", {}).get("silo") + elif parent["type"] == "trash_bin": + _episode_id = None + _silo = None + else: + _episode_id = parent.get("_document", {}).get("data", {}).get("episode_id", None) + _silo = parent.get("_document", {}).get("silo") + else: + _episode_id = None + _silo = None + + dialog.set_episode(_episode_id) + dialog.set_silo(_silo) + + # Set initial values + dialog.set_episode(episode_id) + dialog.set_silo(silo) + + # Signals + model.current_changed.connect(_on_current_asset_changed) + dialog.sequence_created.connect(_on_sequence_created) + + dialog.show() + def on_add_asset(self): """Show add asset dialog""" @@ -426,14 +557,19 @@ def _on_current_asset_changed(): """ parent = model.get_active_asset() - if parent and parent["type"] == "silo": - _parent_id = None - _silo = parent["name"] + if parent: + if parent["type"] == "silo": + _parent_id = None + _silo = parent["name"] + elif parent["type"] == "trash_bin": + _parent_id = None + _silo = None + else: + _parent_id = parent["_id"] + _silo = parent.get("_document", {}).get("silo") else: - _parent_id = parent["_id"] if parent else None + _parent_id = None _silo = None - if parent: - _silo = parent.get("_document", {}).get("silo") dialog.set_parent(_parent_id) dialog.set_silo(_silo) diff --git a/avalon/tools/projectmanager/dialogs.py b/avalon/tools/projectmanager/dialogs.py index febc4227c..9c66cf2cf 100644 --- a/avalon/tools/projectmanager/dialogs.py +++ b/avalon/tools/projectmanager/dialogs.py @@ -137,7 +137,7 @@ def set_parent(self, parent_id): # Get the parent asset (if any provided) if parent_id: - parent_doc = io.find_one({"_id": parent_id, "type": "asset"}) + parent_doc = io.find_one({"_id": parent_id}) assert parent_doc, "Parent asset does not exist." parent_name = parent_doc["name"] @@ -170,8 +170,18 @@ def on_add_asset(self): if self.parent_doc: parent_id = self.parent_doc["_id"] + episode = self.parent_doc.get("episode", "") + episode_id = self.parent_doc.get("data", {}).get("episode_id") + sequence = self.parent_doc["name"] if self.parent_doc["type"] == "sequence"\ + else self.parent_doc.get("sequence", "") + sequence_id = parent_id if self.parent_doc["type"] == "sequence"\ + else self.parent_doc.get("data", {}).get("sequence_id") else: parent_id = None + episode = "" + episode_id = None + sequence = "" + sequence_id = None name = self.data["label"]["name"].text() label = self.data["label"]["label"].text() @@ -196,6 +206,10 @@ def on_add_asset(self): data = { "name": name, "label": label, + "episode": episode, + "episode_id": episode_id, + "sequence": sequence, + "sequence_id": sequence_id, "visualParent": parent_id } @@ -229,3 +243,247 @@ def on_add_asset(self): return self.asset_created.emit(data) + + +class EpisodeCreateDialog(QtWidgets.QDialog): + """A Dialog to create a new episode.""" + + episode_created = QtCore.Signal(dict) + + def __init__(self, is_silo_required=True, parent=None): + super(EpisodeCreateDialog, self).__init__(parent=parent) + self.setWindowTitle("Add episode") + self.is_silo_required = is_silo_required + + # Label + label_label = QtWidgets.QLabel("Label:") + label = widgets.NameValidEdit() + label.setPlaceholderText("